From 2ae38250dd234041db001e93c2d6c6dba9b79ab8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 16 Feb 2016 18:22:12 -0200 Subject: [PATCH 001/177] Deprecated our API in favor of WP API --- .../v1/class-wc-api-authentication.php | 0 .../{ => legacy}/v1/class-wc-api-coupons.php | 0 .../v1/class-wc-api-customers.php | 0 .../v1/class-wc-api-json-handler.php | 0 .../{ => legacy}/v1/class-wc-api-orders.php | 0 .../{ => legacy}/v1/class-wc-api-products.php | 0 .../{ => legacy}/v1/class-wc-api-reports.php | 0 .../{ => legacy}/v1/class-wc-api-resource.php | 0 .../{ => legacy}/v1/class-wc-api-server.php | 0 .../v1/class-wc-api-xml-handler.php | 0 .../v1/interface-wc-api-handler.php | 0 .../v2/class-wc-api-authentication.php | 0 .../{ => legacy/v2}/class-wc-api-coupons.php | 0 .../v2/class-wc-api-customers.php | 0 .../v2}/class-wc-api-exception.php | 0 .../v2}/class-wc-api-json-handler.php | 0 .../{ => legacy}/v2/class-wc-api-orders.php | 0 .../{ => legacy}/v2/class-wc-api-products.php | 0 .../{ => legacy/v2}/class-wc-api-reports.php | 0 .../{ => legacy}/v2/class-wc-api-resource.php | 0 .../{ => legacy}/v2/class-wc-api-server.php | 0 .../{ => legacy}/v2/class-wc-api-webhooks.php | 0 .../v2}/interface-wc-api-handler.php | 0 .../v3}/class-wc-api-authentication.php | 0 .../v3}/class-wc-api-coupons.php | 0 .../v3}/class-wc-api-customers.php | 0 .../v3}/class-wc-api-exception.php | 0 .../v3}/class-wc-api-json-handler.php | 0 .../{ => legacy/v3}/class-wc-api-orders.php | 0 .../{ => legacy/v3}/class-wc-api-products.php | 0 .../v3}/class-wc-api-reports.php | 0 .../{ => legacy/v3}/class-wc-api-resource.php | 0 .../{ => legacy/v3}/class-wc-api-server.php | 0 .../{ => legacy/v3}/class-wc-api-taxes.php | 0 .../{ => legacy/v3}/class-wc-api-webhooks.php | 0 .../v3}/interface-wc-api-handler.php | 0 includes/class-wc-api.php | 90 ++++++++++--------- 37 files changed, 49 insertions(+), 41 deletions(-) rename includes/api/{ => legacy}/v1/class-wc-api-authentication.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-coupons.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-customers.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-json-handler.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-orders.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-products.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-reports.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-resource.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-server.php (100%) rename includes/api/{ => legacy}/v1/class-wc-api-xml-handler.php (100%) rename includes/api/{ => legacy}/v1/interface-wc-api-handler.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-authentication.php (100%) rename includes/api/{ => legacy/v2}/class-wc-api-coupons.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-customers.php (100%) rename includes/api/{ => legacy/v2}/class-wc-api-exception.php (100%) rename includes/api/{ => legacy/v2}/class-wc-api-json-handler.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-orders.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-products.php (100%) rename includes/api/{ => legacy/v2}/class-wc-api-reports.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-resource.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-server.php (100%) rename includes/api/{ => legacy}/v2/class-wc-api-webhooks.php (100%) rename includes/api/{ => legacy/v2}/interface-wc-api-handler.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-authentication.php (100%) rename includes/api/{v2 => legacy/v3}/class-wc-api-coupons.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-customers.php (100%) rename includes/api/{v2 => legacy/v3}/class-wc-api-exception.php (100%) rename includes/api/{v2 => legacy/v3}/class-wc-api-json-handler.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-orders.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-products.php (100%) rename includes/api/{v2 => legacy/v3}/class-wc-api-reports.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-resource.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-server.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-taxes.php (100%) rename includes/api/{ => legacy/v3}/class-wc-api-webhooks.php (100%) rename includes/api/{v2 => legacy/v3}/interface-wc-api-handler.php (100%) diff --git a/includes/api/v1/class-wc-api-authentication.php b/includes/api/legacy/v1/class-wc-api-authentication.php similarity index 100% rename from includes/api/v1/class-wc-api-authentication.php rename to includes/api/legacy/v1/class-wc-api-authentication.php diff --git a/includes/api/v1/class-wc-api-coupons.php b/includes/api/legacy/v1/class-wc-api-coupons.php similarity index 100% rename from includes/api/v1/class-wc-api-coupons.php rename to includes/api/legacy/v1/class-wc-api-coupons.php diff --git a/includes/api/v1/class-wc-api-customers.php b/includes/api/legacy/v1/class-wc-api-customers.php similarity index 100% rename from includes/api/v1/class-wc-api-customers.php rename to includes/api/legacy/v1/class-wc-api-customers.php diff --git a/includes/api/v1/class-wc-api-json-handler.php b/includes/api/legacy/v1/class-wc-api-json-handler.php similarity index 100% rename from includes/api/v1/class-wc-api-json-handler.php rename to includes/api/legacy/v1/class-wc-api-json-handler.php diff --git a/includes/api/v1/class-wc-api-orders.php b/includes/api/legacy/v1/class-wc-api-orders.php similarity index 100% rename from includes/api/v1/class-wc-api-orders.php rename to includes/api/legacy/v1/class-wc-api-orders.php diff --git a/includes/api/v1/class-wc-api-products.php b/includes/api/legacy/v1/class-wc-api-products.php similarity index 100% rename from includes/api/v1/class-wc-api-products.php rename to includes/api/legacy/v1/class-wc-api-products.php diff --git a/includes/api/v1/class-wc-api-reports.php b/includes/api/legacy/v1/class-wc-api-reports.php similarity index 100% rename from includes/api/v1/class-wc-api-reports.php rename to includes/api/legacy/v1/class-wc-api-reports.php diff --git a/includes/api/v1/class-wc-api-resource.php b/includes/api/legacy/v1/class-wc-api-resource.php similarity index 100% rename from includes/api/v1/class-wc-api-resource.php rename to includes/api/legacy/v1/class-wc-api-resource.php diff --git a/includes/api/v1/class-wc-api-server.php b/includes/api/legacy/v1/class-wc-api-server.php similarity index 100% rename from includes/api/v1/class-wc-api-server.php rename to includes/api/legacy/v1/class-wc-api-server.php diff --git a/includes/api/v1/class-wc-api-xml-handler.php b/includes/api/legacy/v1/class-wc-api-xml-handler.php similarity index 100% rename from includes/api/v1/class-wc-api-xml-handler.php rename to includes/api/legacy/v1/class-wc-api-xml-handler.php diff --git a/includes/api/v1/interface-wc-api-handler.php b/includes/api/legacy/v1/interface-wc-api-handler.php similarity index 100% rename from includes/api/v1/interface-wc-api-handler.php rename to includes/api/legacy/v1/interface-wc-api-handler.php diff --git a/includes/api/v2/class-wc-api-authentication.php b/includes/api/legacy/v2/class-wc-api-authentication.php similarity index 100% rename from includes/api/v2/class-wc-api-authentication.php rename to includes/api/legacy/v2/class-wc-api-authentication.php diff --git a/includes/api/class-wc-api-coupons.php b/includes/api/legacy/v2/class-wc-api-coupons.php similarity index 100% rename from includes/api/class-wc-api-coupons.php rename to includes/api/legacy/v2/class-wc-api-coupons.php diff --git a/includes/api/v2/class-wc-api-customers.php b/includes/api/legacy/v2/class-wc-api-customers.php similarity index 100% rename from includes/api/v2/class-wc-api-customers.php rename to includes/api/legacy/v2/class-wc-api-customers.php diff --git a/includes/api/class-wc-api-exception.php b/includes/api/legacy/v2/class-wc-api-exception.php similarity index 100% rename from includes/api/class-wc-api-exception.php rename to includes/api/legacy/v2/class-wc-api-exception.php diff --git a/includes/api/class-wc-api-json-handler.php b/includes/api/legacy/v2/class-wc-api-json-handler.php similarity index 100% rename from includes/api/class-wc-api-json-handler.php rename to includes/api/legacy/v2/class-wc-api-json-handler.php diff --git a/includes/api/v2/class-wc-api-orders.php b/includes/api/legacy/v2/class-wc-api-orders.php similarity index 100% rename from includes/api/v2/class-wc-api-orders.php rename to includes/api/legacy/v2/class-wc-api-orders.php diff --git a/includes/api/v2/class-wc-api-products.php b/includes/api/legacy/v2/class-wc-api-products.php similarity index 100% rename from includes/api/v2/class-wc-api-products.php rename to includes/api/legacy/v2/class-wc-api-products.php diff --git a/includes/api/class-wc-api-reports.php b/includes/api/legacy/v2/class-wc-api-reports.php similarity index 100% rename from includes/api/class-wc-api-reports.php rename to includes/api/legacy/v2/class-wc-api-reports.php diff --git a/includes/api/v2/class-wc-api-resource.php b/includes/api/legacy/v2/class-wc-api-resource.php similarity index 100% rename from includes/api/v2/class-wc-api-resource.php rename to includes/api/legacy/v2/class-wc-api-resource.php diff --git a/includes/api/v2/class-wc-api-server.php b/includes/api/legacy/v2/class-wc-api-server.php similarity index 100% rename from includes/api/v2/class-wc-api-server.php rename to includes/api/legacy/v2/class-wc-api-server.php diff --git a/includes/api/v2/class-wc-api-webhooks.php b/includes/api/legacy/v2/class-wc-api-webhooks.php similarity index 100% rename from includes/api/v2/class-wc-api-webhooks.php rename to includes/api/legacy/v2/class-wc-api-webhooks.php diff --git a/includes/api/interface-wc-api-handler.php b/includes/api/legacy/v2/interface-wc-api-handler.php similarity index 100% rename from includes/api/interface-wc-api-handler.php rename to includes/api/legacy/v2/interface-wc-api-handler.php diff --git a/includes/api/class-wc-api-authentication.php b/includes/api/legacy/v3/class-wc-api-authentication.php similarity index 100% rename from includes/api/class-wc-api-authentication.php rename to includes/api/legacy/v3/class-wc-api-authentication.php diff --git a/includes/api/v2/class-wc-api-coupons.php b/includes/api/legacy/v3/class-wc-api-coupons.php similarity index 100% rename from includes/api/v2/class-wc-api-coupons.php rename to includes/api/legacy/v3/class-wc-api-coupons.php diff --git a/includes/api/class-wc-api-customers.php b/includes/api/legacy/v3/class-wc-api-customers.php similarity index 100% rename from includes/api/class-wc-api-customers.php rename to includes/api/legacy/v3/class-wc-api-customers.php diff --git a/includes/api/v2/class-wc-api-exception.php b/includes/api/legacy/v3/class-wc-api-exception.php similarity index 100% rename from includes/api/v2/class-wc-api-exception.php rename to includes/api/legacy/v3/class-wc-api-exception.php diff --git a/includes/api/v2/class-wc-api-json-handler.php b/includes/api/legacy/v3/class-wc-api-json-handler.php similarity index 100% rename from includes/api/v2/class-wc-api-json-handler.php rename to includes/api/legacy/v3/class-wc-api-json-handler.php diff --git a/includes/api/class-wc-api-orders.php b/includes/api/legacy/v3/class-wc-api-orders.php similarity index 100% rename from includes/api/class-wc-api-orders.php rename to includes/api/legacy/v3/class-wc-api-orders.php diff --git a/includes/api/class-wc-api-products.php b/includes/api/legacy/v3/class-wc-api-products.php similarity index 100% rename from includes/api/class-wc-api-products.php rename to includes/api/legacy/v3/class-wc-api-products.php diff --git a/includes/api/v2/class-wc-api-reports.php b/includes/api/legacy/v3/class-wc-api-reports.php similarity index 100% rename from includes/api/v2/class-wc-api-reports.php rename to includes/api/legacy/v3/class-wc-api-reports.php diff --git a/includes/api/class-wc-api-resource.php b/includes/api/legacy/v3/class-wc-api-resource.php similarity index 100% rename from includes/api/class-wc-api-resource.php rename to includes/api/legacy/v3/class-wc-api-resource.php diff --git a/includes/api/class-wc-api-server.php b/includes/api/legacy/v3/class-wc-api-server.php similarity index 100% rename from includes/api/class-wc-api-server.php rename to includes/api/legacy/v3/class-wc-api-server.php diff --git a/includes/api/class-wc-api-taxes.php b/includes/api/legacy/v3/class-wc-api-taxes.php similarity index 100% rename from includes/api/class-wc-api-taxes.php rename to includes/api/legacy/v3/class-wc-api-taxes.php diff --git a/includes/api/class-wc-api-webhooks.php b/includes/api/legacy/v3/class-wc-api-webhooks.php similarity index 100% rename from includes/api/class-wc-api-webhooks.php rename to includes/api/legacy/v3/class-wc-api-webhooks.php diff --git a/includes/api/v2/interface-wc-api-handler.php b/includes/api/legacy/v3/interface-wc-api-handler.php similarity index 100% rename from includes/api/v2/interface-wc-api-handler.php rename to includes/api/legacy/v3/interface-wc-api-handler.php diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 1ef5e831715..48307ab62d6 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -30,6 +30,7 @@ class WC_API { * The REST API server. * * @var WC_API_Server + * @deprecated 2.6.0 */ public $server; @@ -37,6 +38,7 @@ class WC_API { * REST API authentication class instance. * * @var WC_API_Authentication + * @deprecated 2.6.0 */ public $authentication; @@ -72,8 +74,9 @@ class WC_API { */ public function add_query_vars( $vars ) { $vars[] = 'wc-api'; - $vars[] = 'wc-api-version'; - $vars[] = 'wc-api-route'; + $vars[] = 'wc-api-version'; // Deprecated since 2.6.0. + $vars[] = 'wc-api-route'; // Deprecated since 2.6.0. + return $vars; } @@ -84,7 +87,7 @@ class WC_API { */ public static function add_endpoint() { - // REST API + // REST API, deprecated since 2.6.0. add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' ); add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' ); @@ -97,6 +100,7 @@ class WC_API { * Handle REST API requests. * * @since 2.2 + * @deprecated 2.6.0 */ public function handle_rest_api_requests() { global $wp; @@ -140,27 +144,28 @@ class WC_API { * Include required files for REST API request. * * @since 2.1 + * @deprecated 2.6.0 */ public function includes() { // API server / response handlers - include_once( 'api/class-wc-api-exception.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/legacy/v3/class-wc-api-exception.php' ); + include_once( 'api/legacy/v3/class-wc-api-server.php' ); + include_once( 'api/legacy/v3/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v3/class-wc-api-json-handler.php' ); // authentication - include_once( 'api/class-wc-api-authentication.php' ); + include_once( 'api/legacy/v3/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-coupons.php' ); - include_once( 'api/class-wc-api-customers.php' ); - include_once( 'api/class-wc-api-orders.php' ); - include_once( 'api/class-wc-api-products.php' ); - include_once( 'api/class-wc-api-reports.php' ); - include_once( 'api/class-wc-api-taxes.php' ); - include_once( 'api/class-wc-api-webhooks.php' ); + include_once( 'api/legacy/v3/class-wc-api-resource.php' ); + include_once( 'api/legacy/v3/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v3/class-wc-api-customers.php' ); + include_once( 'api/legacy/v3/class-wc-api-orders.php' ); + include_once( 'api/legacy/v3/class-wc-api-products.php' ); + include_once( 'api/legacy/v3/class-wc-api-reports.php' ); + include_once( 'api/legacy/v3/class-wc-api-taxes.php' ); + include_once( 'api/legacy/v3/class-wc-api-webhooks.php' ); // allow plugins to load other response handlers or resource classes do_action( 'woocommerce_api_loaded' ); @@ -170,6 +175,7 @@ class WC_API { * Register available API resources. * * @since 2.1 + * @deprecated 2.6.0 * @param WC_API_Server $server the REST server */ public function register_resources( $server ) { @@ -196,24 +202,25 @@ class WC_API { * Handle legacy v1 REST API requests. * * @since 2.2 + * @deprecated 2.6.0 */ private function handle_v1_rest_api_request() { // include legacy required files for v1 REST API request - include_once( 'api/v1/class-wc-api-server.php' ); - include_once( 'api/v1/interface-wc-api-handler.php' ); - include_once( 'api/v1/class-wc-api-json-handler.php' ); - include_once( 'api/v1/class-wc-api-xml-handler.php' ); + include_once( 'api/legacy/v1/class-wc-api-server.php' ); + include_once( 'api/legacy/v1/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v1/class-wc-api-json-handler.php' ); + include_once( 'api/legacy/v1/class-wc-api-xml-handler.php' ); - include_once( 'api/v1/class-wc-api-authentication.php' ); + include_once( 'api/legacy/v1/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( 'api/v1/class-wc-api-resource.php' ); - include_once( 'api/v1/class-wc-api-coupons.php' ); - include_once( 'api/v1/class-wc-api-customers.php' ); - include_once( 'api/v1/class-wc-api-orders.php' ); - include_once( 'api/v1/class-wc-api-products.php' ); - include_once( 'api/v1/class-wc-api-reports.php' ); + include_once( 'api/legacy/v1/class-wc-api-resource.php' ); + include_once( 'api/legacy/v1/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v1/class-wc-api-customers.php' ); + include_once( 'api/legacy/v1/class-wc-api-orders.php' ); + include_once( 'api/legacy/v1/class-wc-api-products.php' ); + include_once( 'api/legacy/v1/class-wc-api-reports.php' ); // allow plugins to load other response handlers or resource classes do_action( 'woocommerce_api_loaded' ); @@ -243,23 +250,24 @@ class WC_API { * Handle legacy v2 REST API requests. * * @since 2.4 + * @deprecated 2.6.0 */ private function handle_v2_rest_api_request() { - include_once( 'api/v2/class-wc-api-exception.php' ); - include_once( 'api/v2/class-wc-api-server.php' ); - include_once( 'api/v2/interface-wc-api-handler.php' ); - include_once( 'api/v2/class-wc-api-json-handler.php' ); + include_once( 'api/legacy/v2/class-wc-api-exception.php' ); + include_once( 'api/legacy/v2/class-wc-api-server.php' ); + include_once( 'api/legacy/v2/interface-wc-api-handler.php' ); + include_once( 'api/legacy/v2/class-wc-api-json-handler.php' ); - include_once( 'api/v2/class-wc-api-authentication.php' ); + include_once( 'api/legacy/v2/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); - include_once( 'api/v2/class-wc-api-resource.php' ); - include_once( 'api/v2/class-wc-api-coupons.php' ); - include_once( 'api/v2/class-wc-api-customers.php' ); - include_once( 'api/v2/class-wc-api-orders.php' ); - include_once( 'api/v2/class-wc-api-products.php' ); - include_once( 'api/v2/class-wc-api-reports.php' ); - include_once( 'api/v2/class-wc-api-webhooks.php' ); + include_once( 'api/legacy/v2/class-wc-api-resource.php' ); + include_once( 'api/legacy/v2/class-wc-api-coupons.php' ); + include_once( 'api/legacy/v2/class-wc-api-customers.php' ); + include_once( 'api/legacy/v2/class-wc-api-orders.php' ); + include_once( 'api/legacy/v2/class-wc-api-products.php' ); + include_once( 'api/legacy/v2/class-wc-api-reports.php' ); + include_once( 'api/legacy/v2/class-wc-api-webhooks.php' ); // allow plugins to load other response handlers or resource classes do_action( 'woocommerce_api_loaded' ); @@ -289,8 +297,8 @@ class WC_API { /** * API request - Trigger any API requests. * - * @since 2.0 - * @version 2.4 + * @since 2.0 + * @version 2.4 */ public function handle_api_requests() { global $wp; From f6242506c433efbb35c854bce292d01bc61b87bd Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 16 Feb 2016 18:57:33 -0200 Subject: [PATCH 002/177] Included WP_REST_Controller class from WP-API v2 --- includes/vendor/class-wp-rest-controller.php | 470 +++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 includes/vendor/class-wp-rest-controller.php diff --git a/includes/vendor/class-wp-rest-controller.php b/includes/vendor/class-wp-rest-controller.php new file mode 100644 index 00000000000..486450e4f93 --- /dev/null +++ b/includes/vendor/class-wp-rest-controller.php @@ -0,0 +1,470 @@ + 405 ) ); + } + + /** + * Get a collection of items. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to get a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to create items. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Create one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to update a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Update one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Check if a given request has access to delete a specific item. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Delete one item from the collection. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare the item for create or update operation. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|object $prepared_item + */ + protected function prepare_item_for_database( $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare the item for the REST response. + * + * @param mixed $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + return new WP_Error( 'invalid-method', sprintf( "Method '%s' not implemented. Must be over-ridden in subclass.", __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepare a response for inserting into a collection. + * + * @param WP_REST_Response $response Response object. + * @return array Response data, ready for insertion into collection data. + */ + public function prepare_response_for_collection( $response ) { + if ( ! ( $response instanceof WP_REST_Response ) ) { + return $response; + } + + $data = (array) $response->get_data(); + $links = WP_REST_Server::get_response_links( $response ); + if ( ! empty( $links ) ) { + $data['_links'] = $links; + } + + return $data; + } + + /** + * Filter a response based on the context defined in the schema. + * + * @param array $data + * @param string $context + * @return array + */ + public function filter_response_by_context( $data, $context ) { + + $schema = $this->get_item_schema(); + foreach ( $data as $key => $value ) { + if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) { + continue; + } + + if ( ! in_array( $context, $schema['properties'][ $key ]['context'] ) ) { + unset( $data[ $key ] ); + } + + if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) { + foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) { + if ( empty( $details['context'] ) ) { + continue; + } + if ( ! in_array( $context, $details['context'] ) ) { + unset( $data[ $key ][ $attribute ] ); + } + } + } + } + + return $data; + } + + /** + * Get the item's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + return $this->add_additional_fields_schema( array() ); + } + + /** + * Get the item's schema for display / public consumption purposes. + * + * @return array + */ + public function get_public_item_schema() { + + $schema = $this->get_item_schema(); + + foreach ( $schema['properties'] as &$property ) { + if ( isset( $property['arg_options'] ) ) { + unset( $property['arg_options'] ); + } + } + + return $schema; + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param(), + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'search' => array( + 'description' => 'Limit results to those matching a string.', + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } + + /** + * Get the magical context param. + * + * Ensures consistent description between endpoints, and populates enum from schema. + * + * @param array $args + * @return array + */ + public function get_context_param( $args = array() ) { + $param_details = array( + 'description' => 'Scope under which the request is made; determines fields present in response.', + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $schema = $this->get_item_schema(); + if ( empty( $schema['properties'] ) ) { + return array_merge( $param_details, $args ); + } + $contexts = array(); + foreach ( $schema['properties'] as $key => $attributes ) { + if ( ! empty( $attributes['context'] ) ) { + $contexts = array_merge( $contexts, $attributes['context'] ); + } + } + if ( ! empty( $contexts ) ) { + $param_details['enum'] = array_unique( $contexts ); + rsort( $param_details['enum'] ); + } + return array_merge( $param_details, $args ); + } + + /** + * Add the values from additional fields to a data object. + * + * @param array $object + * @param WP_REST_Request $request + * @return array modified object with additional fields. + */ + protected function add_additional_fields_to_object( $object, $request ) { + + $additional_fields = $this->get_additional_fields(); + + foreach ( $additional_fields as $field_name => $field_options ) { + + if ( ! $field_options['get_callback'] ) { + continue; + } + + $object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() ); + } + + return $object; + } + + /** + * Update the values of additional fields added to a data object. + * + * @param array $object + * @param WP_REST_Request $request + */ + protected function update_additional_fields_for_object( $object, $request ) { + + $additional_fields = $this->get_additional_fields(); + + foreach ( $additional_fields as $field_name => $field_options ) { + + if ( ! $field_options['update_callback'] ) { + continue; + } + + // Don't run the update callbacks if the data wasn't passed in the request. + if ( ! isset( $request[ $field_name ] ) ) { + continue; + } + + call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() ); + } + } + + /** + * Add the schema from additional fields to an schema array. + * + * The type of object is inferred from the passed schema. + * + * @param array $schema Schema array. + */ + protected function add_additional_fields_schema( $schema ) { + if ( empty( $schema['title'] ) ) { + return $schema; + } + + /** + * Can't use $this->get_object_type otherwise we cause an inf loop. + */ + $object_type = $schema['title']; + + $additional_fields = $this->get_additional_fields( $object_type ); + + foreach ( $additional_fields as $field_name => $field_options ) { + if ( ! $field_options['schema'] ) { + continue; + } + + $schema['properties'][ $field_name ] = $field_options['schema']; + } + + return $schema; + } + + /** + * Get all the registered additional fields for a given object-type. + * + * @param string $object_type + * @return array + */ + protected function get_additional_fields( $object_type = null ) { + + if ( ! $object_type ) { + $object_type = $this->get_object_type(); + } + + if ( ! $object_type ) { + return array(); + } + + global $wp_rest_additional_fields; + + if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) { + return array(); + } + + return $wp_rest_additional_fields[ $object_type ]; + } + + /** + * Get the object type this controller is responsible for managing. + * + * @return string + */ + protected function get_object_type() { + $schema = $this->get_item_schema(); + + if ( ! $schema || ! isset( $schema['title'] ) ) { + return null; + } + + return $schema['title']; + } + + /** + * Get an array of endpoint arguments from the item schema for the controller. + * + * @param string $method HTTP method of the request. The arguments + * for `CREATABLE` requests are checked for required + * values and may fall-back to a given default, this + * is not done on `EDITABLE` requests. Default is + * WP_REST_Server::CREATABLE. + * @return array $endpoint_args + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + + $schema = $this->get_item_schema(); + $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); + $endpoint_args = array(); + + foreach ( $schema_properties as $field_id => $params ) { + + // Arguments specified as `readonly` are not allowed to be set. + if ( ! empty( $params['readonly'] ) ) { + continue; + } + + $endpoint_args[ $field_id ] = array( + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'rest_sanitize_request_arg', + ); + + if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { + $endpoint_args[ $field_id ]['default'] = $params['default']; + } + + if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { + $endpoint_args[ $field_id ]['required'] = true; + } + + foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) { + if ( isset( $params[ $schema_prop ] ) ) { + $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; + } + } + + // Merge in any options provided by the schema property. + if ( isset( $params['arg_options'] ) ) { + + // Only use required / default from arg_options on CREATABLE endpoints. + if ( WP_REST_Server::CREATABLE !== $method ) { + $params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) ); + } + + $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); + } + } + + return $endpoint_args; + } + +} From e926c54d2904dedb2679ce916b8313371d15aa60 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 16 Feb 2016 18:59:18 -0200 Subject: [PATCH 003/177] Created WC_REST_Controller and structure to load WooCommerce controllers --- .../abstracts/abstract-wc-rest-controller.php | 20 +++++++ includes/class-wc-api.php | 58 ++++++++++++++++--- 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 includes/abstracts/abstract-wc-rest-controller.php diff --git a/includes/abstracts/abstract-wc-rest-controller.php b/includes/abstracts/abstract-wc-rest-controller.php new file mode 100644 index 00000000000..09704b45a9c --- /dev/null +++ b/includes/abstracts/abstract-wc-rest-controller.php @@ -0,0 +1,20 @@ +rest_api_includes(); + + // Add query vars. add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); - // register API endpoints + // Register API endpoints. add_action( 'init', array( $this, 'add_endpoint' ), 0 ); - // handle REST API requests + // Handle REST API requests. add_action( 'parse_request', array( $this, 'handle_rest_api_requests' ), 0 ); - // handle wc-api endpoint requests + // Handle wc-api endpoint requests. add_action( 'parse_request', array( $this, 'handle_api_requests' ), 0 ); - // Ensure payment gateways are initialized in time for API requests + // Ensure payment gateways are initialized in time for API requests. add_action( 'woocommerce_api_request', array( 'WC_Payment_Gateways', 'instance' ), 0 ); + + // WP REST API. + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); } /** @@ -333,6 +345,38 @@ class WC_API { die('-1'); } } + + /** + * Include REST API classes. + * + * @since 2.6.0 + */ + public function rest_api_includes() { + if ( ! class_exists( 'WP_REST_Controller' ) ) { + include_once( 'vendor/class-wp-rest-controller.php' ); + } + + include_once( 'abstracts/abstract-wc-rest-controller.php' ); + } + + /** + * Register REST API routes. + * + * @since 2.6.0 + */ + public function register_rest_routes() { + $controllers = array(); + + foreach ( $controllers as $controller ) { + $_controller = new $controller(); + if ( ! is_subclass_of( $_controller, 'WC_REST_Controller' ) ) { + continue; + } + + $this->$controller = $_controller; + $this->$controller->register_routes(); + } + } } endif; From 7a1f1ff43c27f3f474f5d0c9994417e137abb188 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 17 Feb 2016 17:29:09 -0200 Subject: [PATCH 004/177] Added initial REST API controllers --- includes/api/wc-rest-coupons-controller.php | 45 +++++++++++++++++++ includes/api/wc-rest-customers-controller.php | 38 ++++++++++++++++ .../api/wc-rest-order-notes-controller.php | 45 +++++++++++++++++++ .../api/wc-rest-order-refunds-controller.php | 45 +++++++++++++++++++ includes/api/wc-rest-orders-controller.php | 45 +++++++++++++++++++ ...est-product-attribute-terms-controller.php | 38 ++++++++++++++++ .../wc-rest-product-attributes-controller.php | 38 ++++++++++++++++ .../wc-rest-product-categories-controller.php | 45 +++++++++++++++++++ ...st-product-shipping-classes-controller.php | 45 +++++++++++++++++++ .../api/wc-rest-product-tags-controller.php | 45 +++++++++++++++++++ includes/api/wc-rest-products-controller.php | 45 +++++++++++++++++++ includes/api/wc-rest-reports-controller.php | 38 ++++++++++++++++ .../api/wc-rest-tax-classes-controller.php | 38 ++++++++++++++++ includes/api/wc-rest-taxes-controller.php | 38 ++++++++++++++++ includes/class-wc-api.php | 31 ++++++++++++- 15 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 includes/api/wc-rest-coupons-controller.php create mode 100644 includes/api/wc-rest-customers-controller.php create mode 100644 includes/api/wc-rest-order-notes-controller.php create mode 100644 includes/api/wc-rest-order-refunds-controller.php create mode 100644 includes/api/wc-rest-orders-controller.php create mode 100644 includes/api/wc-rest-product-attribute-terms-controller.php create mode 100644 includes/api/wc-rest-product-attributes-controller.php create mode 100644 includes/api/wc-rest-product-categories-controller.php create mode 100644 includes/api/wc-rest-product-shipping-classes-controller.php create mode 100644 includes/api/wc-rest-product-tags-controller.php create mode 100644 includes/api/wc-rest-products-controller.php create mode 100644 includes/api/wc-rest-reports-controller.php create mode 100644 includes/api/wc-rest-tax-classes-controller.php create mode 100644 includes/api/wc-rest-taxes-controller.php diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php new file mode 100644 index 00000000000..e80d0c4e160 --- /dev/null +++ b/includes/api/wc-rest-coupons-controller.php @@ -0,0 +1,45 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Order_Notes_Controller extends WC_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $base = 'orders/(?P[\d]+)/notes'; + + /** + * Type of object. + * + * @var string + */ + protected $object = 'order_note'; + + /** + * Register the routes for order notes. + */ + public function register_routes() { + + } +} diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..d9a54150d57 --- /dev/null +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -0,0 +1,45 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $base = 'orders/(?P[\d]+)/refunds'; + + /** + * Type of object. + * + * @var string + */ + protected $object = 'shop_order_refund'; + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + + } +} diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php new file mode 100644 index 00000000000..b3076147dd7 --- /dev/null +++ b/includes/api/wc-rest-orders-controller.php @@ -0,0 +1,45 @@ +[\d]+)/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Controller + */ +class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Controller { + + /** + * Route base. + * + * @var string + */ + protected $base = 'products/attributes/(?P[\d]+)/terms'; + + /** + * Register the routes for coupons. + */ + public function register_routes() { + + } +} diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..e697034024d --- /dev/null +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -0,0 +1,38 @@ + Date: Wed, 17 Feb 2016 17:40:41 -0200 Subject: [PATCH 005/177] Load WP REST API endpoints only for WP 4.4 or later --- includes/class-wc-api.php | 81 ++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index ae7ee943ed6..955edd5883e 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -55,9 +55,6 @@ class WC_API { * @return WC_API */ public function __construct() { - // Include REST API classes. - $this->rest_api_includes(); - // Add query vars. add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); @@ -74,7 +71,7 @@ class WC_API { add_action( 'woocommerce_api_request', array( 'WC_Payment_Gateways', 'instance' ), 0 ); // WP REST API. - add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + $this->rest_api_init(); } /** @@ -103,7 +100,7 @@ class WC_API { add_rewrite_rule( '^wc-api/v([1-3]{1})/?$', 'index.php?wc-api-version=$matches[1]&wc-api-route=/', 'top' ); add_rewrite_rule( '^wc-api/v([1-3]{1})(.*)?', 'index.php?wc-api-version=$matches[1]&wc-api-route=$matches[2]', 'top' ); - // WC API for payment gateway IPNs, etc + // WC API for payment gateway IPNs, etc. add_rewrite_endpoint( 'wc-api', EP_ALL ); } @@ -125,13 +122,13 @@ class WC_API { $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; } - // REST API request + // REST API request. if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { define( 'WC_API_REQUEST', true ); define( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); - // legacy v1 API request + // Legacy v1 API request. if ( 1 === WC_API_REQUEST_VERSION ) { $this->handle_v1_rest_api_request(); } else if ( 2 === WC_API_REQUEST_VERSION ) { @@ -141,10 +138,10 @@ class WC_API { $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); - // load API resource classes + // load API resource classes. $this->register_resources( $this->server ); - // Fire off the request + // Fire off the request. $this->server->serve_request(); } @@ -160,13 +157,13 @@ class WC_API { */ public function includes() { - // API server / response handlers + // API server / response handlers. include_once( 'api/legacy/v3/class-wc-api-exception.php' ); include_once( 'api/legacy/v3/class-wc-api-server.php' ); include_once( 'api/legacy/v3/interface-wc-api-handler.php' ); include_once( 'api/legacy/v3/class-wc-api-json-handler.php' ); - // authentication + // Authentication. include_once( 'api/legacy/v3/class-wc-api-authentication.php' ); $this->authentication = new WC_API_Authentication(); @@ -179,7 +176,7 @@ class WC_API { include_once( 'api/legacy/v3/class-wc-api-taxes.php' ); include_once( 'api/legacy/v3/class-wc-api-webhooks.php' ); - // allow plugins to load other response handlers or resource classes + // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); } @@ -218,7 +215,7 @@ class WC_API { */ private function handle_v1_rest_api_request() { - // include legacy required files for v1 REST API request + // Include legacy required files for v1 REST API request. include_once( 'api/legacy/v1/class-wc-api-server.php' ); include_once( 'api/legacy/v1/interface-wc-api-handler.php' ); include_once( 'api/legacy/v1/class-wc-api-json-handler.php' ); @@ -234,12 +231,12 @@ class WC_API { include_once( 'api/legacy/v1/class-wc-api-products.php' ); include_once( 'api/legacy/v1/class-wc-api-reports.php' ); - // allow plugins to load other response handlers or resource classes + // Allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); - // Register available resources for legacy v1 REST API request + // Register available resources for legacy v1 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', @@ -254,7 +251,7 @@ class WC_API { $this->$api_class = new $api_class( $this->server ); } - // Fire off the request + // Fire off the request. $this->server->serve_request(); } @@ -281,12 +278,12 @@ class WC_API { include_once( 'api/legacy/v2/class-wc-api-reports.php' ); include_once( 'api/legacy/v2/class-wc-api-webhooks.php' ); - // allow plugins to load other response handlers or resource classes + // allow plugins to load other response handlers or resource classes. do_action( 'woocommerce_api_loaded' ); $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); - // Register available resources for legacy v2 REST API request + // Register available resources for legacy v2 REST API request. $api_classes = apply_filters( 'woocommerce_api_classes', array( 'WC_API_Customers', @@ -302,7 +299,7 @@ class WC_API { $this->$api_class = new $api_class( $this->server ); } - // Fire off the request + // Fire off the request. $this->server->serve_request(); } @@ -319,39 +316,58 @@ class WC_API { $wp->query_vars['wc-api'] = $_GET['wc-api']; } - // wc-api endpoint requests + // wc-api endpoint requests. if ( ! empty( $wp->query_vars['wc-api'] ) ) { - // Buffer, we won't want any output here + // Buffer, we won't want any output here. ob_start(); - // No cache headers + // No cache headers. nocache_headers(); - // Clean the API request + // Clean the API request. $api_request = strtolower( wc_clean( $wp->query_vars['wc-api'] ) ); - // Trigger generic action before request hook + // Trigger generic action before request hook. do_action( 'woocommerce_api_request', $api_request ); - // Is there actually something hooked into this API request? If not trigger 400 - Bad request + // Is there actually something hooked into this API request? If not trigger 400 - Bad request. status_header( has_action( 'woocommerce_api_' . $api_request ) ? 200 : 400 ); - // Trigger an action which plugins can hook into to fulfill the request + // Trigger an action which plugins can hook into to fulfill the request. do_action( 'woocommerce_api_' . $api_request ); - // Done, clear buffer and exit + // Done, clear buffer and exit. ob_end_clean(); - die('-1'); + die( '-1' ); } } + /** + * Init WP REST API. + * + * @since 2.6.0 + */ + private function rest_api_init() { + global $wp_version; + + // REST API was included starting WordPress 4.4. + if ( version_compare( $wp_version, 4.4, '<' ) ) { + return; + } + + $this->rest_api_includes(); + + // Init REST API routes. + add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); + } + /** * Include REST API classes. * * @since 2.6.0 */ - public function rest_api_includes() { + private function rest_api_includes() { if ( ! class_exists( 'WP_REST_Controller' ) ) { include_once( 'vendor/class-wp-rest-controller.php' ); } @@ -397,12 +413,7 @@ class WC_API { ); foreach ( $controllers as $controller ) { - $_controller = new $controller(); - if ( ! is_subclass_of( $_controller, 'WC_REST_Controller' ) ) { - continue; - } - - $this->$controller = $_controller; + $this->$controller = new $controller(); $this->$controller->register_routes(); } } From 92ef7303b72c6cf94cbd08d37b6c26d6eaa6f1dc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 22 Feb 2016 15:49:38 -0300 Subject: [PATCH 006/177] Rename $base to $rest_base --- includes/api/wc-rest-coupons-controller.php | 2 +- includes/api/wc-rest-customers-controller.php | 2 +- includes/api/wc-rest-order-notes-controller.php | 2 +- includes/api/wc-rest-order-refunds-controller.php | 2 +- includes/api/wc-rest-orders-controller.php | 2 +- includes/api/wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/wc-rest-product-attributes-controller.php | 2 +- includes/api/wc-rest-product-categories-controller.php | 2 +- includes/api/wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/wc-rest-product-tags-controller.php | 2 +- includes/api/wc-rest-products-controller.php | 2 +- includes/api/wc-rest-reports-controller.php | 2 +- includes/api/wc-rest-tax-classes-controller.php | 2 +- includes/api/wc-rest-taxes-controller.php | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index e80d0c4e160..0cc42dd9d44 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -27,7 +27,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'coupons'; + protected $rest_base = 'coupons'; /** * Type of object. diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 7dbcabd5950..e3fabdc60dc 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -27,7 +27,7 @@ class WC_REST_Customers_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'customers'; + protected $rest_base = 'customers'; /** * Register the routes for customers. diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 7e34f0995b3..9cf527a4a17 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Notes_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'orders/(?P[\d]+)/notes'; + protected $rest_base = 'orders/(?P[\d]+)/notes'; /** * Type of object. diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php index d9a54150d57..aa317ee0abb 100644 --- a/includes/api/wc-rest-order-refunds-controller.php +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'orders/(?P[\d]+)/refunds'; + protected $rest_base = 'orders/(?P[\d]+)/refunds'; /** * Type of object. diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index b3076147dd7..faf7908cd8e 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -27,7 +27,7 @@ class WC_REST_Orders_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'orders'; + protected $rest_base = 'orders'; /** * Type of object. diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 4546eee906e..47e66605128 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products/attributes/(?P[\d]+)/terms'; + protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index e697034024d..54319449cd3 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attributes_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products/attributes'; + protected $rest_base = 'products/attributes'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index 46a695a0d9d..d9aa1529738 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products/categories'; + protected $rest_base = 'products/categories'; /** * Type of object. diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index 2f41a1875a0..82fb55a6186 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products/shipping_classes'; + protected $rest_base = 'products/shipping_classes'; /** * Type of object. diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 9e79a3f378e..01645b830ee 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products/tags'; + protected $rest_base = 'products/tags'; /** * Type of object. diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/wc-rest-products-controller.php index 80e9dcdd693..d7030b51c12 100644 --- a/includes/api/wc-rest-products-controller.php +++ b/includes/api/wc-rest-products-controller.php @@ -27,7 +27,7 @@ class WC_REST_Products_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'products'; + protected $rest_base = 'products'; /** * Type of object. diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index 35d582a6eca..9e3c588a18a 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -27,7 +27,7 @@ class WC_REST_Reports_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'reports'; + protected $rest_base = 'reports'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index abb5d4607be..e017879d556 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Tax_Classes_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'taxes/classes'; + protected $rest_base = 'taxes/classes'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 1fcc351df99..d73e2279f1b 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Taxes_Controller extends WC_REST_Controller { * * @var string */ - protected $base = 'taxes'; + protected $rest_base = 'taxes'; /** * Register the routes for coupons. From 43a5f6f8420710b6da438ad180db387e58ca206b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 22 Feb 2016 16:43:52 -0300 Subject: [PATCH 007/177] Created abstract classes for posts and terms --- .../abstracts/abstract-wc-rest-controller.php | 20 -- .../abstract-wc-rest-posts-controller.php | 173 ++++++++++++++++++ .../abstract-wc-rest-terms-controller.php | 23 +++ includes/api/wc-rest-coupons-controller.php | 8 +- includes/api/wc-rest-customers-controller.php | 4 +- .../api/wc-rest-order-notes-controller.php | 8 +- .../api/wc-rest-order-refunds-controller.php | 8 +- includes/api/wc-rest-orders-controller.php | 8 +- ...est-product-attribute-terms-controller.php | 4 +- .../wc-rest-product-attributes-controller.php | 4 +- .../wc-rest-product-categories-controller.php | 4 +- ...st-product-shipping-classes-controller.php | 4 +- .../api/wc-rest-product-tags-controller.php | 4 +- includes/api/wc-rest-products-controller.php | 8 +- includes/api/wc-rest-reports-controller.php | 4 +- .../api/wc-rest-tax-classes-controller.php | 4 +- includes/api/wc-rest-taxes-controller.php | 4 +- includes/class-wc-api.php | 7 +- 18 files changed, 240 insertions(+), 59 deletions(-) delete mode 100644 includes/abstracts/abstract-wc-rest-controller.php create mode 100644 includes/abstracts/abstract-wc-rest-posts-controller.php create mode 100644 includes/abstracts/abstract-wc-rest-terms-controller.php diff --git a/includes/abstracts/abstract-wc-rest-controller.php b/includes/abstracts/abstract-wc-rest-controller.php deleted file mode 100644 index 09704b45a9c..00000000000 --- a/includes/abstracts/abstract-wc-rest-controller.php +++ /dev/null @@ -1,20 +0,0 @@ -ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( sprintf( 'woocommerce_rest_api_invalid_%s_id', $this->post_type ), __( 'Invalid id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $data = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Check if a given request has access to read a post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { + return new WP_Error( 'rest_forbidden_context', sprintf( __( 'Sorry, you are not allowed to edit %s', 'woocommerce' ), $this->rest_base ), array( 'status' => rest_authorization_required_code() ) ); + } + + if ( $post ) { + return $this->check_read_permission( $post ); + } + + return true; + } + + /** + * Check if we can read a post. + * + * Correctly handles posts with the inherit status. + * + * @param object $post Post object. + * @return boolean Can we read it? + */ + public function check_read_permission( $post ) { + return $this->check_permission( $post, 'read' ); + } + + /** + * Check if we can edit a post. + * + * @param object $post Post object. + * @return boolean Can we edit it? + */ + protected function check_update_permission( $post ) { + return $this->check_permission( $post, 'edit' ); + } + + /** + * Check if we can create a post. + * + * @param object $post Post object. + * @return boolean Can we create it?. + */ + protected function check_create_permission( $post ) { + return $this->check_permission( $post, 'edit' ); + } + + /** + * Check if we can delete a post. + * + * @param object $post Post object. + * @return boolean Can we delete it? + */ + protected function check_delete_permission( $post ) { + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context. + * + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + $permission = false; + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return $permission; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } elseif ( 'edit' === $context ) { + $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); + } + + return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); + } + + /** + * Check the post_date_gmt or modified_gmt and prepare any post or + * modified date for single post output. + * + * @param string $date_gmt + * @param string|null $date + * @return string|null ISO8601/RFC3339 formatted datetime. + */ + protected function prepare_date_response( $date_gmt, $date = null ) { + // Use the date if passed. + if ( isset( $date ) ) { + return mysql_to_rfc3339( $date ); + } + // Return null if $date_gmt is empty/zeros. + if ( '0000-00-00 00:00:00' === $date_gmt ) { + return null; + } + // Return the formatted datetime. + return mysql_to_rfc3339( $date_gmt ); + } +} diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php new file mode 100644 index 00000000000..5d87e85996f --- /dev/null +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -0,0 +1,23 @@ +[\d]+)/notes'; /** - * Type of object. + * Post type. * * @var string */ - protected $object = 'order_note'; + protected $post_type = 'order_note'; /** * Register the routes for order notes. diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php index aa317ee0abb..766949db862 100644 --- a/includes/api/wc-rest-order-refunds-controller.php +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Order Refunds controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Posts_Controller */ -class WC_REST_Order_Refunds_Controller extends WC_REST_Controller { +class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { /** * Route base. @@ -30,11 +30,11 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Controller { protected $rest_base = 'orders/(?P[\d]+)/refunds'; /** - * Type of object. + * Post type. * * @var string */ - protected $object = 'shop_order_refund'; + protected $post_type = 'shop_order_refund'; /** * Register the routes for order refunds. diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index faf7908cd8e..7efbb972b1c 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Orders controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Posts_Controller */ -class WC_REST_Orders_Controller extends WC_REST_Controller { +class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { /** * Route base. @@ -30,11 +30,11 @@ class WC_REST_Orders_Controller extends WC_REST_Controller { protected $rest_base = 'orders'; /** - * Type of object. + * Post type. * * @var string */ - protected $object = 'shop_order'; + protected $post_type = 'shop_order'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 47e66605128..7077d53067c 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Product Attribute Terms controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Terms_Controller */ -class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Controller { +class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controller { /** * Route base. diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 54319449cd3..d954d70273d 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Product Attributes controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WP_REST_Controller */ -class WC_REST_Product_Attributes_Controller extends WC_REST_Controller { +class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { /** * Route base. diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index d9aa1529738..bf64812f10e 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Product Categories controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Terms_Controller */ -class WC_REST_Product_Categories_Controller extends WC_REST_Controller { +class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { /** * Route base. diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index 82fb55a6186..0d066428527 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Product Shipping Classes controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Terms_Controller */ -class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Controller { +class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controller { /** * Route base. diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 01645b830ee..0c9103b38f0 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Product Tags controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Terms_Controller */ -class WC_REST_Product_Tags_Controller extends WC_REST_Controller { +class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { /** * Route base. diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/wc-rest-products-controller.php index d7030b51c12..8b54bf41ef5 100644 --- a/includes/api/wc-rest-products-controller.php +++ b/includes/api/wc-rest-products-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Products controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WC_REST_Posts_Controller */ -class WC_REST_Products_Controller extends WC_REST_Controller { +class WC_REST_Products_Controller extends WC_REST_Posts_Controller { /** * Route base. @@ -30,11 +30,11 @@ class WC_REST_Products_Controller extends WC_REST_Controller { protected $rest_base = 'products'; /** - * Type of object. + * Post type. * * @var string */ - protected $object = 'product'; + protected $post_type = 'product'; /** * Register the routes for coupons. diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index 9e3c588a18a..dcf31ffc744 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Reports controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WP_REST_Controller */ -class WC_REST_Reports_Controller extends WC_REST_Controller { +class WC_REST_Reports_Controller extends WP_REST_Controller { /** * Route base. diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index e017879d556..8fb40940966 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Tax Classes controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WP_REST_Controller */ -class WC_REST_Tax_Classes_Controller extends WC_REST_Controller { +class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { /** * Route base. diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index d73e2279f1b..cb14cfb9803 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Taxes controller class. * * @package WooCommerce/API - * @extends WC_REST_Controller + * @extends WP_REST_Controller */ -class WC_REST_Taxes_Controller extends WC_REST_Controller { +class WC_REST_Taxes_Controller extends WP_REST_Controller { /** * Route base. diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 955edd5883e..a76712af671 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -368,11 +368,16 @@ class WC_API { * @since 2.6.0 */ private function rest_api_includes() { + // WP REST Controller (vendor while not implemented in WP core). if ( ! class_exists( 'WP_REST_Controller' ) ) { include_once( 'vendor/class-wp-rest-controller.php' ); } - include_once( 'abstracts/abstract-wc-rest-controller.php' ); + // Abstract controllers. + include_once( 'abstracts/abstract-wc-rest-posts-controller.php' ); + include_once( 'abstracts/abstract-wc-rest-terms-controller.php' ); + + // REST API controllers. include_once( 'api/wc-rest-coupons-controller.php' ); include_once( 'api/wc-rest-customers-controller.php' ); include_once( 'api/wc-rest-order-notes-controller.php' ); From 54739c5e19eec7aa9f544ba6b6bd1ad71a24245e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 22 Feb 2016 16:44:47 -0300 Subject: [PATCH 008/177] Created GET coupons endpoint --- includes/api/wc-rest-coupons-controller.php | 69 +++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index e121571d09f..b3295ba6b55 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -40,6 +40,75 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * Register the routes for coupons. */ public function register_routes() { + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + ) ); + } + /** + * Prepare a single coupon output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + global $wpdb; + + // 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'", $post->ID ) ); + + $coupon = new WC_Coupon( $code ); + + $data = array( + 'id' => $coupon->id, + 'code' => $coupon->code, + 'type' => $coupon->type, + 'created_at' => $this->prepare_date_response( $post->post_date_gmt ), + 'updated_at' => $this->prepare_date_response( $post->post_modified_gmt ), + 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), + 'individual_use' => ( 'yes' === $coupon->individual_use ), + 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), + 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, + 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, + 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, + 'usage_count' => (int) $coupon->usage_count, + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->prepare_date_response( $coupon->expiry_date ) : null, + 'enable_free_shipping' => $coupon->enable_free_shipping(), + 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), + 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), + 'exclude_sale_items' => $coupon->exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), + 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), + 'customer_emails' => $coupon->customer_email, + 'description' => $post->post_excerpt, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_api_prepare_' . $this->post_type, $response, $post, $request ); } } From a31ef08ed7aee525f1c014e21c047eb7bce556ea Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 22 Feb 2016 17:48:38 -0300 Subject: [PATCH 009/177] Improved permissions check --- .../abstract-wc-rest-posts-controller.php | 122 +++++------------- 1 file changed, 30 insertions(+), 92 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 4143f6acbfe..5a3e579be9b 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -35,6 +35,35 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { */ protected $public = false; + /** + * Check if a given request has access to read a item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + + if ( $post ) { + $post_type = get_post_type_object( $this->post_type ); + return 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } + + return true; + } + + /** + * Check if a given request has access to read items. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $post_type = get_post_type_object( $this->post_type ); + + return current_user_can( $post_type->cap->read_private_posts ); + } + /** * Get a single item. * @@ -48,6 +77,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { return new WP_Error( sprintf( 'woocommerce_rest_api_invalid_%s_id', $this->post_type ), __( 'Invalid id.', 'woocommerce' ), array( 'status' => 404 ) ); } + $data = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $data ); @@ -58,98 +88,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } - /** - * Check if a given request has access to read a post. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $post = get_post( (int) $request['id'] ); - if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( 'rest_forbidden_context', sprintf( __( 'Sorry, you are not allowed to edit %s', 'woocommerce' ), $this->rest_base ), array( 'status' => rest_authorization_required_code() ) ); - } - - if ( $post ) { - return $this->check_read_permission( $post ); - } - - return true; - } - - /** - * Check if we can read a post. - * - * Correctly handles posts with the inherit status. - * - * @param object $post Post object. - * @return boolean Can we read it? - */ - public function check_read_permission( $post ) { - return $this->check_permission( $post, 'read' ); - } - - /** - * Check if we can edit a post. - * - * @param object $post Post object. - * @return boolean Can we edit it? - */ - protected function check_update_permission( $post ) { - return $this->check_permission( $post, 'edit' ); - } - - /** - * Check if we can create a post. - * - * @param object $post Post object. - * @return boolean Can we create it?. - */ - protected function check_create_permission( $post ) { - return $this->check_permission( $post, 'edit' ); - } - - /** - * Check if we can delete a post. - * - * @param object $post Post object. - * @return boolean Can we delete it? - */ - protected function check_delete_permission( $post ) { - return $this->check_permission( $post, 'delete' ); - } - - /** - * Checks the permissions for the current user given a post and context. - * - * @param WP_Post|int $post - * @param string $context the type of permission to check, either `read`, `write`, or `delete` - * @return bool true if the current user has the permissions to perform the context on the post - */ - private function check_permission( $post, $context ) { - $permission = false; - - if ( ! is_a( $post, 'WP_Post' ) ) { - $post = get_post( $post ); - } - - if ( is_null( $post ) ) { - return $permission; - } - - $post_type = get_post_type_object( $post->post_type ); - - if ( 'read' === $context ) { - $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); - } elseif ( 'edit' === $context ) { - $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); - } elseif ( 'delete' === $context ) { - $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); - } - - return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); - } - /** * Check the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. From f6e9d85ff202e1e0e146d1b2131a085a6ffb6e01 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 12:07:08 -0300 Subject: [PATCH 010/177] Added version to the WP_REST_Controller class --- includes/vendor/class-wp-rest-controller.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/vendor/class-wp-rest-controller.php b/includes/vendor/class-wp-rest-controller.php index 486450e4f93..9633a039a87 100644 --- a/includes/vendor/class-wp-rest-controller.php +++ b/includes/vendor/class-wp-rest-controller.php @@ -1,6 +1,12 @@ Date: Tue, 23 Feb 2016 12:15:25 -0300 Subject: [PATCH 011/177] Included wp-api functions --- includes/class-wc-api.php | 3 +- includes/vendor/wp-api-functions.php | 198 +++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 includes/vendor/wp-api-functions.php diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index a76712af671..062e308276b 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -368,7 +368,8 @@ class WC_API { * @since 2.6.0 */ private function rest_api_includes() { - // WP REST Controller (vendor while not implemented in WP core). + // WP-API classes and functions. + include_once( 'vendor/wp-api-functions.php' ); if ( ! class_exists( 'WP_REST_Controller' ) ) { include_once( 'vendor/class-wp-rest-controller.php' ); } diff --git a/includes/vendor/wp-api-functions.php b/includes/vendor/wp-api-functions.php new file mode 100644 index 00000000000..20dd2e74b8a --- /dev/null +++ b/includes/vendor/wp-api-functions.php @@ -0,0 +1,198 @@ + null, + 'update_callback' => null, + 'schema' => null, + ); + + $args = wp_parse_args( $args, $defaults ); + + global $wp_rest_additional_fields; + + $object_types = (array) $object_type; + + foreach ( $object_types as $object_type ) { + $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args; + } + } +} + +if ( ! function_exists( 'register_api_field' ) ) { + /** + * Backwards compat shim + */ + function register_api_field( $object_type, $attributes, $args = array() ) { + _deprecated_function( 'register_api_field', 'WPAPI-2.0', 'register_rest_field' ); + register_rest_field( $object_type, $attributes, $args ); + } +} + +if ( ! function_exists( 'rest_validate_request_arg' ) ) { + /** + * Validate a request argument based on details registered to the route. + * + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return WP_Error|boolean + */ + function rest_validate_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return true; + } + $args = $attributes['args'][ $param ]; + + if ( ! empty( $args['enum'] ) ) { + if ( ! in_array( $value, $args['enum'] ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s' ), $param, implode( ', ', $args['enum'] ) ) ); + } + } + + if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $param, 'integer' ) ); + } + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $param, 'string' ) ); + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + if ( ! rest_parse_date( $value ) ) { + return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) ); + } + break; + + case 'email' : + if ( ! is_email( $value ) ) { + return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) ); + } + break; + } + } + + if ( in_array( $args['type'], array( 'numeric', 'integer' ) ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) { + if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (exclusive)' ), $param, $args['minimum'] ) ); + } else if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (inclusive)' ), $param, $args['minimum'] ) ); + } + } else if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (exclusive)' ), $param, $args['maximum'] ) ); + } else if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (inclusive)' ), $param, $args['maximum'] ) ); + } + } else if ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } else if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } + } + } + + return true; + } +} + +if ( ! function_exists( 'rest_sanitize_request_arg' ) ) { + /** + * Sanitize a request argument based on details registered to the route. + * + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return mixed + */ + function rest_sanitize_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return $value; + } + $args = $attributes['args'][ $param ]; + + if ( 'integer' === $args['type'] ) { + return (int) $value; + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + return sanitize_text_field( $value ); + + case 'email' : + /* + * sanitize_email() validates, which would be unexpected + */ + return sanitize_text_field( $value ); + + case 'uri' : + return esc_url_raw( $value ); + } + } + + return $value; + } + +} From 0d88e3c380e5bebd4d2425827a60ca4943e5c8ea Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 12:16:21 -0300 Subject: [PATCH 012/177] Added endpoint to get coupons --- .../abstract-wc-rest-posts-controller.php | 318 +++++++++++++++++- includes/api/wc-rest-coupons-controller.php | 11 +- 2 files changed, 324 insertions(+), 5 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 5a3e579be9b..611c0d0edb0 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -36,7 +36,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { protected $public = false; /** - * Check if a given request has access to read a item. + * Check if a given request has access to read an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -45,13 +45,25 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $post = get_post( (int) $request['id'] ); if ( $post ) { - $post_type = get_post_type_object( $this->post_type ); - return 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + return $this->check_read_permission( $post ); } return true; } + /** + * Check if we can read an item. + * + * Correctly handles posts with the inherit status. + * + * @param object $post Post object. + * @return boolean Can we read it? + */ + public function check_read_permission( $post ) { + $post_type = get_post_type_object( $this->post_type ); + return 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } + /** * Check if a given request has access to read items. * @@ -75,7 +87,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { - return new WP_Error( sprintf( 'woocommerce_rest_api_invalid_%s_id', $this->post_type ), __( 'Invalid id.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'Invalid id.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = $this->prepare_item_for_response( $post, $request ); @@ -88,6 +100,304 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + + $args['date_query'] = array(); + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $request['before'] ) ) { + $args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $request['after'] ) ) { + $args['date_query'][0]['after'] = $request['after']; + } + + if ( is_array( $request['filter'] ) ) { + $args = array_merge( $args, $request['filter'] ); + unset( $args['filter'] ); + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request ); + $query_args = $this->prepare_items_query( $args, $request ); + + $posts_query = new WP_Query(); + $query_result = $posts_query->query( $query_args ); + + $posts = array(); + foreach ( $query_result as $post ) { + if ( ! $this->check_read_permission( $post ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $post, $request ); + $posts[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $total_posts = $posts_query->found_posts; + + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] ); + + $response = rest_ensure_response( $posts ); + $response->header( 'X-WP-Total', (int) $total_posts ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $request_params = $request->get_query_params(); + if ( ! empty( $request_params['filter'] ) ) { + // Normalize the pagination params. + unset( $request_params['filter']['posts_per_page'] ); + unset( $request_params['filter']['paged'] ); + } + $base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ) ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Determine the allowed query_vars for a get_items() response and + * prepare for WP_Query. + * + * @param array $prepared_args + * @param WP_REST_Request $request + * @return array $query_args + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $valid_vars = array_flip( $this->get_allowed_query_vars() ); + $query_args = array(); + foreach ( $valid_vars as $var => $index ) { + if ( isset( $prepared_args[ $var ] ) ) { + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $var, refers to the query_var key. + * + * @param mixed $prepared_args[ $var ] The query_var value. + * + */ + $query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] ); + } + } + + $query_args['ignore_sticky_posts'] = true; + + if ( 'include' === $query_args['orderby'] ) { + $query_args['orderby'] = 'post__in'; + } + + return $query_args; + } + + /** + * Get all the WP Query vars that are allowed for the API request. + * + * @return array + */ + protected function get_allowed_query_vars() { + global $wp; + + /** + * Filter the publicly allowed query vars. + * + * Allows adjusting of the default query vars that are made public. + * + * @param array Array of allowed WP_Query query vars. + */ + $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars ); + + $post_type_obj = get_post_type_object( $this->post_type ); + if ( current_user_can( $post_type_obj->cap->edit_posts ) ) { + /** + * Filter the allowed 'private' query vars for authorized users. + * + * If the user has the `edit_posts` capability, we also allow use of + * private query parameters, which are only undesirable on the + * frontend, but are safe for use in query strings. + * + * To disable anyway, use + * `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );` + * + * @param array $private_query_vars Array of allowed query vars for authorized users. + * } + */ + $private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars ); + $valid_vars = array_merge( $valid_vars, $private ); + } + // Define our own in addition to WP's normal vars. + $rest_valid = array( + 'date_query', + 'ignore_sticky_posts', + 'offset', + 'post__in', + 'post__not_in', + 'post_parent', + 'post_parent__in', + 'post_parent__not_in', + 'posts_per_page', + ); + $valid_vars = array_merge( $valid_vars, $rest_valid ); + + /** + * Filter allowed query vars for the REST API. + * + * This filter allows you to add or remove query vars from the final allowed + * list for all requests, including unauthenticated ones. To alter the + * vars for editors only. + * + * @param array { + * Array of allowed WP_Query query vars. + * + * @param string $allowed_query_var The query var to allow. + * } + */ + $valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars ); + + return $valid_vars; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $post_type_obj = get_post_type_object( $this->post_type ); + if ( $post_type_obj->hierarchical ) { + $params['parent'] = array( + 'description' => _( 'Limit result set to those of particular parent ids.', 'woocommerce' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => _( 'Limit result set to all items except those of a particular parent id.', 'woocommerce' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + $params['slug'] = array( + 'description' => __( 'Limit result set to posts with a specific slug.', 'woocommerce', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + $params['filter'] = array( + 'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ), + ); + + return $params; + } + /** * Check the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index b3295ba6b55..015f6862a5b 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -40,6 +40,15 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * Register the routes for coupons. */ public function register_routes() { + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + ) ); + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, @@ -109,6 +118,6 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ - return apply_filters( 'woocommerce_rest_api_prepare_' . $this->post_type, $response, $post, $request ); + return apply_filters( 'woocommerce_rest_prepare_' . $this->post_type, $response, $post, $request ); } } From f10846277b77784eb9b2d767d6e1b438c82b984f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 15:28:43 -0300 Subject: [PATCH 013/177] Added method to delete coupons --- .../abstract-wc-rest-posts-controller.php | 110 +++++++++++++++++- includes/api/wc-rest-coupons-controller.php | 11 ++ 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 611c0d0edb0..e70c256450d 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -51,6 +51,35 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access to read items. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $post_type = get_post_type_object( $this->post_type ); + + return current_user_can( $post_type->cap->read_private_posts ); + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + + $post = get_post( $request['id'] ); + + if ( $post && ! $this->check_delete_permission( $post ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if we can read an item. * @@ -65,15 +94,14 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to read items. + * Check if we can delete a post. * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean + * @param object $post Post object. + * @return boolean Can we delete it? */ - public function get_items_permissions_check( $request ) { + protected function check_delete_permission( $post ) { $post_type = get_post_type_object( $this->post_type ); - - return current_user_can( $post_type->cap->read_private_posts ); + return current_user_can( $post_type->cap->delete_post, $post->ID ); } /** @@ -398,6 +426,76 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $params; } + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0; + + /** + * Filter whether an item is trashable. + * + * Return false to disable trash support for the item. + * + * @param boolean $supports_trash Whether the item type support trashing. + * @param WP_Post $post The Post object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); + + if ( ! $this->check_delete_permission( $post ) ) { + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $result = wp_delete_post( $id, true ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' === $post->post_status ) { + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.' ), $this->post_type ), array( 'status' => 410 ) ); + } + + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $result = wp_trash_post( $id ); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + return $response; + } + /** * Check the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 015f6862a5b..5ecabc0942f 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -58,6 +58,17 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), ) ); } From 9ba3747b74139b7f082fa7efa2fcfba98f2e7678 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 15:34:14 -0300 Subject: [PATCH 014/177] Fixed controller for order notes --- includes/api/wc-rest-order-notes-controller.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 2412bd3b928..b7baa5f143f 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @package WooCommerce/API * @extends WC_REST_Posts_Controller */ -class WC_REST_Order_Notes_Controller extends WC_REST_Posts_Controller { +class WC_REST_Order_Notes_Controller extends WP_REST_Controller { /** * Route base. @@ -29,13 +29,6 @@ class WC_REST_Order_Notes_Controller extends WC_REST_Posts_Controller { */ protected $rest_base = 'orders/(?P[\d]+)/notes'; - /** - * Post type. - * - * @var string - */ - protected $post_type = 'order_note'; - /** * Register the routes for order notes. */ From 1fb4108e08976d3332a78488fd90bf9ee8a0deb1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 15:34:42 -0300 Subject: [PATCH 015/177] Added methods to delete orders, refunds and products --- .../api/wc-rest-order-refunds-controller.php | 14 +++++++++++++- includes/api/wc-rest-orders-controller.php | 16 ++++++++++++++-- includes/api/wc-rest-products-controller.php | 16 ++++++++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php index 766949db862..984818837ba 100644 --- a/includes/api/wc-rest-order-refunds-controller.php +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -40,6 +40,18 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { * Register the routes for order refunds. */ public function register_routes() { - + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + ) ); } } diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 7efbb972b1c..59f6ea95b94 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -37,9 +37,21 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { protected $post_type = 'shop_order'; /** - * Register the routes for coupons. + * Register the routes for orders. */ public function register_routes() { - + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + ) ); } } diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/wc-rest-products-controller.php index 8b54bf41ef5..185d980086b 100644 --- a/includes/api/wc-rest-products-controller.php +++ b/includes/api/wc-rest-products-controller.php @@ -37,9 +37,21 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { protected $post_type = 'product'; /** - * Register the routes for coupons. + * Register the routes for products. */ public function register_routes() { - + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + ) ); } } From 759328582412f4a519d6c8edbd165dfc11adfe8e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 23 Feb 2016 15:34:55 -0300 Subject: [PATCH 016/177] Fixed docblocks --- includes/api/wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/wc-rest-product-attributes-controller.php | 2 +- includes/api/wc-rest-product-categories-controller.php | 2 +- includes/api/wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/wc-rest-product-tags-controller.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 7077d53067c..21cf5f03a06 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -30,7 +30,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; /** - * Register the routes for coupons. + * Register the routes for product attribute terms. */ public function register_routes() { diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index d954d70273d..dd463281385 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -30,7 +30,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { protected $rest_base = 'products/attributes'; /** - * Register the routes for coupons. + * Register the routes for product attributes. */ public function register_routes() { diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index bf64812f10e..e92f41c8ccf 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -37,7 +37,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { protected $object = 'product_cat'; /** - * Register the routes for coupons. + * Register the routes for product categories. */ public function register_routes() { diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index 0d066428527..eb86b0a7e69 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -37,7 +37,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll protected $object = 'product_shipping_class'; /** - * Register the routes for coupons. + * Register the routes for product shipping classes. */ public function register_routes() { diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 0c9103b38f0..71263144a32 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -37,7 +37,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { protected $object = 'product_tag'; /** - * Register the routes for coupons. + * Register the routes for product tags. */ public function register_routes() { From 53db5ff4c8f2ab3605019e394d7cc75a3eca9cba Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 26 Feb 2016 17:24:33 -0300 Subject: [PATCH 017/177] Created coupons POST and PUT endpoints and coupons schema --- .../abstract-wc-rest-posts-controller.php | 220 ++++++++++- includes/api/wc-rest-coupons-controller.php | 370 ++++++++++++++++++ 2 files changed, 588 insertions(+), 2 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index e70c256450d..89151ac7218 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -51,6 +51,23 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->create_posts ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', sprintf( __( 'Sorry, you are not allowed to create a new %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read items. * @@ -63,6 +80,23 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return current_user_can( $post_type->cap->read_private_posts ); } + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( $request['id'] ); + $post_type = get_post_type_object( $this->post_type ); + + if ( $post && ! $this->check_update_permission( $post ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', sprintf( __( 'Sorry, you are not allowed to update this %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );; + } + + return true; + } + /** * Check if a given request has access to delete an item. * @@ -70,7 +104,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { - $post = get_post( $request['id'] ); if ( $post && ! $this->check_delete_permission( $post ) ) { @@ -94,7 +127,18 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } /** - * Check if we can delete a post. + * Check if we can edit an item. + * + * @param object $post Post object. + * @return boolean Can we edit it? + */ + protected function check_update_permission( $post ) { + $post_type = get_post_type_object( $this->post_type ); + return current_user_can( $post_type->cap->edit_post, $post->ID ); + } + + /** + * Check if we can delete an item. * * @param object $post Post object. * @return boolean Can we delete it? @@ -128,6 +172,178 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + + $schema = $this->get_item_schema(); + + // if ( ! empty( $schema['properties']['sticky'] ) ) { + // if ( ! empty( $request['sticky'] ) ) { + // stick_post( $post_id ); + // } else { + // unstick_post( $post_id ); + // } + // } + + // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + // $this->handle_featured_media( $request['featured_media'], $post->ID ); + // } + + // $terms_update = $this->handle_terms( $post->ID, $request ); + // if ( is_wp_error( $terms_update ) ) { + // return $terms_update; + // } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + // Add meta fields. + $meta_fields = $this->add_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + // Remove post. + wp_delete_post( $post->ID, true ); + + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $post_id ) ) ); + + return $response; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return true; + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $schema = $this->get_item_schema(); + + // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + // $this->handle_featured_media( $request['featured_media'], $post_id ); + // } + + // if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) { + // if ( ! empty( $request['sticky'] ) ) { + // stick_post( $post_id ); + // } else { + // unstick_post( $post_id ); + // } + // } + + // $terms_update = $this->handle_terms( $post->ID, $request ); + // if ( is_wp_error( $terms_update ) ) { + // return $terms_update; + // } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + // Update meta fields. + $meta_fields = $this->update_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + return true; + } + /** * Get a collection of posts. * diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 5ecabc0942f..1eab89d3ef4 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -47,6 +47,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( @@ -58,6 +65,12 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -69,6 +82,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { ), ), ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } @@ -131,4 +145,360 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { */ return apply_filters( 'woocommerce_rest_prepare_' . $this->post_type, $response, $post, $request ); } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_missing_parameter', sprintf( __( 'Missing parameter %s.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Code. + if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { + $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); + $id = isset( $data->ID ) ? $data->ID : 0; + + // Check for duplicate coupon codes. + $coupon_found = $wpdb->get_var( $wpdb->prepare( " + SELECT $wpdb->posts.ID + FROM $wpdb->posts + WHERE $wpdb->posts.post_type = 'shop_coupon' + AND $wpdb->posts.post_status = 'publish' + AND $wpdb->posts.post_title = '%s' + AND $wpdb->posts.ID != %s + ", $coupon_code, $id ) ); + + if ( $coupon_found ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data->post_title = $coupon_code; + } + + // Content. + $data->post_content = ''; + + // Excerpt. + if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['description'] ); + } + + // Post type. + $data->post_type = $this->post_type; + + // Post status. + $data->post_status = 'publish'; + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Expiry date format. + * + * @param string $expiry_date + * @return string + */ + protected function get_coupon_expiry_date( $expiry_date ) { + if ( '' != $expiry_date ) { + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + + return ''; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + $data = $request->get_json_params(); + + $defaults = array( + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'enable_free_shipping' => false, + 'product_category_ids' => array(), + 'exclude_product_category_ids' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_emails' => array(), + 'description' => '' + ); + + $data = wp_parse_args( $data, $defaults ); + + // Set coupon meta. + update_post_meta( $post->ID, 'discount_type', $data['type'] ); + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'usage_limit', absint( $data['usage_limit'] ) ); + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $post->ID, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); + + return true; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + if ( isset( $request['amount'] ) ) { + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $request['amount'] ) ); + } + + if ( isset( $request['individual_use'] ) ) { + update_post_meta( $post->ID, 'individual_use', ( true === $request['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_ids'] ) ) { + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $request['product_ids'] ) ) ) ); + } + + if ( isset( $request['exclude_product_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $request['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $request['usage_limit'] ) ) { + update_post_meta( $post->ID, 'usage_limit', absint( $request['usage_limit'] ) ); + } + + if ( isset( $request['usage_limit_per_user'] ) ) { + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $request['usage_limit_per_user'] ) ); + } + + if ( isset( $request['limit_usage_to_x_items'] ) ) { + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $request['limit_usage_to_x_items'] ) ); + } + + if ( isset( $request['usage_count'] ) ) { + update_post_meta( $post->ID, 'usage_count', absint( $request['usage_count'] ) ); + } + + if ( isset( $request['expiry_date'] ) ) { + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); + } + + if ( isset( $request['enable_free_shipping'] ) ) { + update_post_meta( $post->ID, 'free_shipping', ( true === $request['enable_free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_category_ids'] ) ) { + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_category_ids'] ) ) ); + } + + if ( isset( $request['exclude_product_category_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['exclude_product_category_ids'] ) ) ); + } + + if ( isset( $request['exclude_sale_items'] ) ) { + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $request['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['minimum_amount'] ) ) { + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $request['minimum_amount'] ) ); + } + + if ( isset( $request['maximum_amount'] ) ) { + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); + } + + if ( isset( $request['customer_emails'] ) ) { + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['customer_emails'] ) ) ); + } + } + + /** + * Get the Post's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'created_at' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated_at' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'The amount of discount.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'individual_use' => array( + 'description' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product ID's the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_ids' => array( + 'description' => __( "List of product ID's the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be user per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'enable_free_shipping' => array( + 'description' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_category_ids' => array( + 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_category_ids' => array( + 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'Define if should not apply when have sale items.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'customer_emails' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema );; + } } From 26c325acced59037ad265e41a5a595b855f4c31c Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 1 Mar 2016 17:53:25 -0300 Subject: [PATCH 018/177] Fixed __() functions --- includes/abstracts/abstract-wc-rest-posts-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 89151ac7218..ef08092d7f7 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -616,13 +616,13 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $post_type_obj = get_post_type_object( $this->post_type ); if ( $post_type_obj->hierarchical ) { $params['parent'] = array( - 'description' => _( 'Limit result set to those of particular parent ids.', 'woocommerce' ), + 'description' => __( 'Limit result set to those of particular parent ids.', 'woocommerce' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), ); $params['parent_exclude'] = array( - 'description' => _( 'Limit result set to all items except those of a particular parent id.', 'woocommerce' ), + 'description' => __( 'Limit result set to all items except those of a particular parent id.', 'woocommerce' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_id_list', 'default' => array(), From d22dab6c79f7c48c886ab0b3f0402507b3962da5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 1 Mar 2016 17:59:02 -0300 Subject: [PATCH 019/177] Improved required coupon POST params --- includes/api/wc-rest-coupons-controller.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 1eab89d3ef4..5381465e8dd 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -51,7 +51,11 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'required' => true, + ), + ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -390,7 +394,6 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Coupon code.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'required' => true, ), 'type' => array( 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), From c0affe96e030bebcbee3b797af75eb30891d8887 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 1 Mar 2016 19:41:59 -0300 Subject: [PATCH 020/177] Created API functions --- .../abstract-wc-rest-posts-controller.php | 21 --------- includes/api/wc-rest-coupons-controller.php | 6 +-- includes/wc-api-functions.php | 45 +++++++++++++++++++ includes/wc-core-functions.php | 1 + 4 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 includes/wc-api-functions.php diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index ef08092d7f7..6b00447acbb 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -711,25 +711,4 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } - - /** - * Check the post_date_gmt or modified_gmt and prepare any post or - * modified date for single post output. - * - * @param string $date_gmt - * @param string|null $date - * @return string|null ISO8601/RFC3339 formatted datetime. - */ - protected function prepare_date_response( $date_gmt, $date = null ) { - // Use the date if passed. - if ( isset( $date ) ) { - return mysql_to_rfc3339( $date ); - } - // Return null if $date_gmt is empty/zeros. - if ( '0000-00-00 00:00:00' === $date_gmt ) { - return null; - } - // Return the formatted datetime. - return mysql_to_rfc3339( $date_gmt ); - } } diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 5381465e8dd..8e5dec36b83 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -109,8 +109,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, - 'created_at' => $this->prepare_date_response( $post->post_date_gmt ), - 'updated_at' => $this->prepare_date_response( $post->post_modified_gmt ), + 'created_at' => wc_api_prepare_date_response( $post->post_date_gmt ), + 'updated_at' => wc_api_prepare_date_response( $post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), @@ -119,7 +119,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, - 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->prepare_date_response( $coupon->expiry_date ) : null, + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_api_prepare_date_response( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), diff --git a/includes/wc-api-functions.php b/includes/wc-api-functions.php new file mode 100644 index 00000000000..4ca5229944d --- /dev/null +++ b/includes/wc-api-functions.php @@ -0,0 +1,45 @@ + Date: Tue, 1 Mar 2016 20:07:05 -0300 Subject: [PATCH 021/177] Created functions to fetch customer last order and to grap gravatar url --- includes/wc-user-functions.php | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/includes/wc-user-functions.php b/includes/wc-user-functions.php index be06b7799ea..333401c0bbf 100644 --- a/includes/wc-user-functions.php +++ b/includes/wc-user-functions.php @@ -644,3 +644,51 @@ function wc_set_user_last_update_time( $user_id ) { function wc_get_customer_saved_methods_list( $customer_id ) { return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); } + +/** + * Get info about customer's last order. + * + * @since 2.6.0 + * @param int $customer_id Customer ID. + * @return WC_Order|bool Order object if successful or false. + */ +function wc_get_customer_last_order( $customer_id ) { + global $wpdb; + + $customer_id = absint( $customer_id ); + + $id = $wpdb->get_var( "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 = {$customer_id} + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) + ORDER BY posts.ID DESC + " ); + + return wc_get_order( $id ); +} + +/** + * Wrapper for @see get_avatar() which doesn't simply return + * the URL so we need to pluck it from the HTML img tag. + * + * Kudos to https://github.com/WP-API/WP-API for offering a better solution. + * + * @since 2.6.0 + * @param string $email the customer's email. + * @return string the URL to the customer's avatar. + */ +function wc_get_customer_avatar_url( $email ) { + $avatar_html = get_avatar( $email ); + + // Get the URL of the avatar from the provided HTML. + preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); + + if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { + return esc_url_raw( $matches[1] ); + } + + return null; +} From 2a429edbc2a3b9c1d657df520f951f64e7e085c1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 1 Mar 2016 20:07:20 -0300 Subject: [PATCH 022/177] Endpoints for get a single customer and the current customer --- includes/api/wc-rest-customers-controller.php | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 105adf6d8a2..20f39c6bc32 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -33,6 +33,453 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * Register the routes for customers. */ public function register_routes() { + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + ), + 'password' => array( + 'required' => 'yes' === get_option( 'woocommerce_registration_generate_password', 'no' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), array( + 'password' => array(), + ) ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/me', array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_current_item' ), + 'args' => array( + 'context' => array(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check if a given request has access to read a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + $customer = get_userdata( $id ); + $types = get_post_types( array( 'public' => true ), 'names' ); + + if ( empty( $id ) || empty( $customer->ID ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( get_current_user_id() === $id ) { + return true; + } + + if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } else if ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $customer = get_userdata( $id ); + + if ( empty( $id ) || empty( $customer->ID ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $customer ); + + return $response; + } + + /** + * Get the current customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_current_item( $request ) { + $current_customer_id = get_current_user_id(); + if ( empty( $current_customer_id ) ) { + return new WP_Error( 'woocommerce_rest_not_logged_in', __( 'You are not currently logged in.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $customer = wp_get_current_user(); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $current_customer_id ) ) ); + $response->set_status( 302 ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $customer Customer object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $customer, $request ) { + $last_order = wc_get_customer_last_order( $customer->ID ); + + $data = array( + 'id' => $customer->ID, + 'created_at' => wc_api_prepare_date_response( $customer->user_registered ), + 'updated_at' => wc_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ), + 'email' => $customer->user_email, + 'first_name' => $customer->first_name, + 'last_name' => $customer->last_name, + 'username' => $customer->user_login, + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->id : null, + 'date' => is_object( $last_order ) ? wc_api_prepare_date_response( $last_order->post->post_date_gmt ) : null + ), + 'orders_count' => wc_get_customer_order_count( $customer->ID ), + 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), + 'avatar_url' => wc_get_customer_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, + ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $customer ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $customer User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $customer, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_User $customer User object. + * @return array Links for the given user. + */ + protected function prepare_links( $customer ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $customer->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the User's schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + global $wp_roles; + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_at' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated_at' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'last_order' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Last order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date' => array( + 'description' => __( 'UTC DateTime of the customer last order.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing_address' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_address' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From c934b321641b7bc4804b66e7b4b6fc25774847da Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 17:58:40 -0300 Subject: [PATCH 023/177] Created endpoint to get customers --- includes/api/wc-rest-customers-controller.php | 162 +++++++++++++++++- 1 file changed, 155 insertions(+), 7 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 20f39c6bc32..403439d0fe9 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -35,15 +35,15 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { public function register_routes() { register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'args' => $this->get_collection_params(), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'args' => $this->get_collection_params(), ), array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_item' ), + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( 'email' => array( 'required' => true, ), @@ -128,6 +128,97 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return true; } + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( '' !== $prepared_args['search'] ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + if ( ! empty( $request['slug'] ) ) { + $prepared_args['search'] = $request['slug']; + $prepared_args['search_columns'] = array( 'user_nicename' ); + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + /** * Get a single customer. * @@ -182,7 +273,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $data = array( 'id' => $customer->ID, 'created_at' => wc_api_prepare_date_response( $customer->user_registered ), - 'updated_at' => wc_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ), + 'updated_at' => $customer->last_update ? wc_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, @@ -482,4 +573,61 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'view'; + + $query_params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $query_params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $query_params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['slug'] = array( + 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + return $query_params; + } } From b797779fbf6ce56eca65180c4090a0d002f4ec77 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 18:07:23 -0300 Subject: [PATCH 024/177] Required list_users to list customers --- includes/api/wc-rest-customers-controller.php | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 403439d0fe9..8f24207a915 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -35,9 +35,10 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { public function register_routes() { register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'args' => $this->get_collection_params(), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, @@ -100,6 +101,20 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { ) ); } + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'list_users' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list customers.' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read a customer. * @@ -163,6 +178,9 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $prepared_args['search_columns'] = array( 'user_nicename' ); } + // Show only customers. + $prepared_args['role'] = 'customer'; + /** * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. * From d48442f4ed1ed61f2e0c8470fb85d70442a3d1dc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 18:13:46 -0300 Subject: [PATCH 025/177] Allow delete customers --- includes/api/wc-rest-customers-controller.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 8f24207a915..ea31e27f504 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -143,6 +143,22 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! current_user_can( 'delete_user', $id ) ) { + return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get all customers. * @@ -257,6 +273,57 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return $response; } + /** + * Delete a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $customer = get_userdata( $id ); + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'woocommerce_rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $result = wp_delete_user( $id, $reassign ); + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a customer is deleted via the REST API. + * + * @param WP_User $customer The customer data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_customer', $customer, $response, $request ); + + return $response; + } + /** * Get the current customer. * From 36b38fc6e021dfa58484da99f260af99385daa6a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 19:03:09 -0300 Subject: [PATCH 026/177] Fixed return type --- includes/api/wc-rest-coupons-controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 8e5dec36b83..cec3828a1d4 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -370,6 +370,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { if ( isset( $request['customer_emails'] ) ) { update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['customer_emails'] ) ) ); } + + return true; } /** From 8fe79359257d5614ec78a05adc316d931077bf89 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 19:03:18 -0300 Subject: [PATCH 027/177] Fixed textdomains --- includes/abstracts/abstract-wc-rest-posts-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 6b00447acbb..4eef216b1ca 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -688,7 +688,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { // Otherwise, only trash if we haven't already. if ( 'trash' === $post->post_status ) { - return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.' ), $this->post_type ), array( 'status' => 410 ) ); + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); } // (Note that internally this falls through to `wp_delete_post` if @@ -697,7 +697,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } if ( ! $result ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.' ), $this->post_type ), array( 'status' => 500 ) ); + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); } /** From 0b0aa474219890d3fd94d011a15bfa6539bc22db Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 19:14:03 -0300 Subject: [PATCH 028/177] Created methods to create and update customers --- .../abstract-wc-rest-posts-controller.php | 2 +- includes/api/wc-rest-customers-controller.php | 184 +++++++++++++++++- 2 files changed, 175 insertions(+), 11 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 4eef216b1ca..c53288f8b2d 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -326,7 +326,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating item, false when updating. */ - do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $post, $request ); diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index ea31e27f504..4f940971433 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -52,7 +52,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), ), 'password' => array( - 'required' => 'yes' === get_option( 'woocommerce_registration_generate_password', 'no' ), + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), ), ) ), ), @@ -72,9 +72,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), array( - 'password' => array(), - ) ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, @@ -83,7 +81,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'args' => array( 'force' => array( 'default' => false, - 'description' => __( 'Required to be true, as resource does not support trashing.' ), + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), 'reassign' => array(), ), @@ -109,7 +107,21 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list customers.' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list customers.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'create_users' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_customer', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -137,7 +149,23 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } else if ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! current_user_can( 'edit_user', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -153,7 +181,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $id = (int) $request['id']; if ( ! current_user_can( 'delete_user', $id ) ) { - return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -253,6 +281,54 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return $response; } + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer_id = wc_create_new_customer( $request['email'], $request['username'], $request['password'] );; + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + $customer = get_user_by( 'id', $customer_id ); + + $this->update_additional_fields_for_object( $customer, $request ); + + // Add customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $customer, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $customer_id ) ) ); + + return $response; + } + /** * Get a single customer. * @@ -273,6 +349,58 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return $response; } + /** + * Update a single user + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + + $customer = get_userdata( $id ); + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->user_email ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->user_login ) { + return new WP_Error( 'woocommerce_rest_user_invalid_argument', __( "Username isn't editable", 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_email' => sanitize_email( $request['email'] ) ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_pass' => wc_clean( $request['password'] ) ) ); + } + + $this->update_additional_fields_for_object( $customer, $request ); + + // Update customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $customer, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + return $response; + } + /** * Delete a single customer. * @@ -415,6 +543,44 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return apply_filters( 'woocommerce_rest_prepare_customer', $response, $customer, $request ); } + /** + * Update customer meta fields. + * + * @param WP_User $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + $schema = $this->get_item_schema(); + + // Customer first name. + if ( isset( $request['first_name'] ) ) { + update_user_meta( $customer->ID, 'first_name', wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + update_user_meta( $customer->ID, 'last_name', wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing_address'] ) ) { + foreach ( array_keys( $schema['properties']['billing_address']['properties'] ) as $address ) { + if ( isset( $request['billing_address'][ $address ] ) ) { + update_user_meta( $customer->ID, 'billing_' . $address, wc_clean( $request['billing_address'][ $address ] ) ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping_address'] ) ) { + foreach ( array_keys( $schema['properties']['shipping_address']['properties'] ) as $address ) { + if ( isset( $request['shipping_address'][ $address ] ) ) { + update_user_meta( $customer->ID, 'shipping_' . $address, wc_clean( $request['shipping_address'][ $address ] ) ); + } + } + } + } + /** * Prepare links for the request. * @@ -440,8 +606,6 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return array */ public function get_item_schema() { - global $wp_roles; - $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'customer', From 1f8e07108f9d54a43cbfc44be3608bb270a20f54 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 20:22:12 -0300 Subject: [PATCH 029/177] Created methods to list and get terms --- .../abstract-wc-rest-terms-controller.php | 399 ++++++++++++++++++ 1 file changed, 399 insertions(+) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 5d87e85996f..2f4eb0b5681 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -20,4 +20,403 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @var string */ protected $rest_base = ''; + + /** + * Taxonomy. + * + * @var string + */ + protected $taxonomy = ''; + + /** + * Register the routes for terms. + */ + public function register_routes() { + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + + if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy->cap->edit_terms ) ) { + return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } + + /** + * Get terms associated with a taxonomy. + * + * @param WP_REST_Request $request Full details about the request + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $prepared_args = array( + 'exclude' => $request['exclude'], + 'include' => $request['include'], + 'order' => $request['order'], + 'orderby' => $request['orderby'], + 'product' => $request['product'], + 'hide_empty' => $request['hide_empty'], + 'number' => $request['per_page'], + 'search' => $request['search'], + 'slug' => $request['slug'], + ); + + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + + $taxonomy = get_taxonomy( $this->taxonomy ); + + if ( $taxonomy->hierarchical && isset( $request['parent'] ) ) { + if ( 0 === $request['parent'] ) { + // Only query top-level terms. + $prepared_args['parent'] = 0; + } else { + if ( $request['parent'] ) { + $prepared_args['parent'] = $request['parent']; + } + } + } + + /** + * Filter the query arguments, before passing them to `get_terms()`. + * + * Enables adding extra arguments or setting defaults for a terms + * collection request. + * + * @see https://developer.wordpress.org/reference/functions/get_terms/ + * + * @param array $prepared_args Array of arguments to be + * passed to get_terms. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( "woocommerce_rest_{$this->taxonomy}_query", $prepared_args, $request ); + + if ( ! empty( $prepared_args['product'] ) ) { + $query_result = $this->get_terms_for_product( $prepared_args ); + $total_terms = $this->total_terms; + } else { + $query_result = get_terms( $this->taxonomy, $prepared_args ); + + $count_args = $prepared_args; + unset( $count_args['number'] ); + unset( $count_args['offset'] ); + $total_terms = wp_count_terms( $this->taxonomy, $count_args ); + + // Ensure we don't return results when offset is out of bounds. + // See https://core.trac.wordpress.org/ticket/35935 + if ( $prepared_args['offset'] >= $total_terms ) { + $query_result = array(); + } + + // wp_count_terms can return a falsy value when the term has no children. + if ( ! $total_terms ) { + $total_terms = 0; + } + } + $response = array(); + foreach ( $query_result as $term ) { + $data = $this->prepare_item_for_response( $term, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $response ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $response->header( 'X-WP-Total', (int) $total_terms ); + $max_pages = ceil( $total_terms / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Get a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $term = get_term( (int) $request['id'], $this->taxonomy ); + + if ( ! $term || $term->taxonomy !== $this->taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( is_wp_error( $term ) ) { + return $term; + } + + $response = $this->prepare_item_for_response( $term, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare links for the request. + * + * @param object $term Term object. + * @return array Links for the given term. + */ + protected function prepare_links( $term ) { + $base = '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + if ( $term->parent ) { + $parent_term = get_term( (int) $term->parent, $term->taxonomy ); + if ( $parent_term ) { + $links['up'] = array( + 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + ); + } + } + + return $links; + } + + /** + * Get the terms attached to a product. + * + * This is an alternative to `get_terms()` that uses `get_the_terms()` + * instead, which hits the object cache. There are a few things not + * supported, notably `include`, `exclude`. In `self::get_items()` these + * are instead treated as a full query. + * + * @param array $prepared_args Arguments for `get_terms()`. + * @return array List of term objects. (Total count in `$this->total_terms`). + */ + protected function get_terms_for_product( $prepared_args ) { + $query_result = get_the_terms( $prepared_args['product'], $this->taxonomy ); + if ( empty( $query_result ) ) { + $this->total_terms = 0; + return array(); + } + + // get_items() verifies that we don't have `include` set, and default. + // ordering is by `name`. + if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ) ) ) { + switch ( $prepared_args['orderby'] ) { + case 'id' : + $this->sort_column = 'term_id'; + break; + + case 'slug' : + case 'term_group' : + case 'description' : + case 'count' : + $this->sort_column = $prepared_args['orderby']; + break; + } + usort( $query_result, array( $this, 'compare_terms' ) ); + } + if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { + $query_result = array_reverse( $query_result ); + } + + // Pagination. + $this->total_terms = count( $query_result ); + $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); + + return $query_result; + } + + /** + * Comparison function for sorting terms by a column. + * + * Uses `$this->sort_column` to determine field to sort by. + * + * @param stdClass $left Term object. + * @param stdClass $right Term object. + * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. + */ + protected function compare_terms( $left, $right ) { + $col = $this->sort_column; + $left_val = $left->$col; + $right_val = $right->$col; + + if ( is_int( $left_val ) && is_int( $right_val ) ) { + return $left_val - $right_val; + } + + return strcmp( $left_val, $right_val ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + $taxonomy = get_taxonomy( $this->taxonomy ); + + $query_params['context']['default'] = 'view'; + + $query_params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $query_params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + if ( ! $taxonomy->hierarchical ) { + $query_params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + $query_params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'asc', + 'enum' => array( + 'asc', + 'desc', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['orderby'] = array( + 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'name', + 'enum' => array( + 'id', + 'include', + 'name', + 'slug', + 'term_group', + 'description', + 'count', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['hide_empty'] = array( + 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + if ( $taxonomy->hierarchical ) { + $query_params['parent'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific parent.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + $query_params['product'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['slug'] = array( + 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + return $query_params; + } } From 860819d39f22c524a87e8cdced6bb478ca167a5c Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 20:22:29 -0300 Subject: [PATCH 030/177] List and get product categories --- .../wc-rest-product-categories-controller.php | 119 +++++++++++++++++- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index e92f41c8ccf..086572c81a7 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -30,16 +30,127 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { protected $rest_base = 'products/categories'; /** - * Type of object. + * Taxonomy. * * @var string */ - protected $object = 'product_cat'; + protected $taxonomy = 'product_cat'; /** - * Register the routes for product categories. + * Prepare a single product category output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response */ - public function register_routes() { + public function prepare_item_for_response( $item, $request ) { + // Get category display type. + $display_type = get_woocommerce_term_meta( $item->term_id, 'display_type' ); + // Get category image. + $image = ''; + if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_id' ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The id for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From c60ef4ba6239c3c5f50b13c2250d6194bb26d12b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 20:28:31 -0300 Subject: [PATCH 031/177] List and get product tags --- .../wc-rest-product-categories-controller.php | 4 +- .../api/wc-rest-product-tags-controller.php | 90 ++++++++++++++++++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index 086572c81a7..ee248aa88cc 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -65,8 +65,8 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 71263144a32..86b2f760fa7 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -30,16 +30,98 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { protected $rest_base = 'products/tags'; /** - * Type of object. + * Taxonomy. * * @var string */ - protected $object = 'product_tag'; + protected $taxonomy = 'product_tag'; /** - * Register the routes for product tags. + * Prepare a single product tag output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response */ - public function register_routes() { + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From cbdddd99ccbf2c9b4aafd85605bbbd0e2073e8ae Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 2 Mar 2016 20:30:54 -0300 Subject: [PATCH 032/177] List and get product shipping classes --- ...st-product-shipping-classes-controller.php | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index eb86b0a7e69..be4e0218681 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -30,16 +30,104 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll protected $rest_base = 'products/shipping_classes'; /** - * Type of object. + * Taxonomy. * * @var string */ - protected $object = 'product_shipping_class'; + protected $taxonomy = 'product_shipping_class'; /** - * Register the routes for product shipping classes. + * Prepare a single product shipping class output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response */ - public function register_routes() { + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Shipping class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The id for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From a7b1393955c37738cd813c71ea214854fdc71055 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 3 Mar 2016 18:09:37 -0300 Subject: [PATCH 033/177] Required permissions to list or get shipping classes --- ...st-product-shipping-classes-controller.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index be4e0218681..66e14638fc4 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -36,6 +36,31 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll */ protected $taxonomy = 'product_shipping_class'; + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + + return current_user_can( $taxonomy->cap->edit_terms ); + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + $taxonomy = get_taxonomy( $this->taxonomy ); + + return current_user_can( $taxonomy->cap->edit_terms, $id ); + } + /** * Prepare a single product shipping class output for response. * From d4d91d0fe979c26f3d0c5f5dbc61c66e8bcbb57b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 3 Mar 2016 18:19:02 -0300 Subject: [PATCH 034/177] Allow delete categories, tags and shipping classes --- .../abstract-wc-rest-terms-controller.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 2f4eb0b5681..6395d35c291 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -108,6 +108,26 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return $this->get_items_permissions_check( $request ); } + /** + * Check if a given request has access to delete a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $term = get_term( (int) $request['id'], $this->taxonomy ); + if ( ! $term ) { + return new WP_Error( "woocommerce_rest_{$this->taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy = get_taxonomy( $this->taxonomy ); + if ( ! current_user_can( $taxonomy->cap->delete_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get terms associated with a taxonomy. * @@ -238,6 +258,41 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return rest_ensure_response( $response ); } + /** + * Delete a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $term = get_term( (int) $request['id'], $this->taxonomy ); + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $term, $request ); + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->taxonomy}", $term, $response, $request ); + + return $response; + } + /** * Prepare links for the request. * From bf286dfe96a7794697061f27dd38325bfe66f214 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 3 Mar 2016 18:42:40 -0300 Subject: [PATCH 035/177] Created api functions to upload images --- includes/api/wc-rest-coupons-controller.php | 6 +- includes/api/wc-rest-customers-controller.php | 6 +- includes/wc-api-functions.php | 105 +++++++++++++++++- 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index cec3828a1d4..6ebc0255052 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -109,8 +109,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, - 'created_at' => wc_api_prepare_date_response( $post->post_date_gmt ), - 'updated_at' => wc_api_prepare_date_response( $post->post_modified_gmt ), + 'created_at' => wc_rest_api_prepare_date_response( $post->post_date_gmt ), + 'updated_at' => wc_rest_api_prepare_date_response( $post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), @@ -119,7 +119,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, - 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_api_prepare_date_response( $coupon->expiry_date ) : null, + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_api_prepare_date_response( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 4f940971433..b6ad77074be 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -485,15 +485,15 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $data = array( 'id' => $customer->ID, - 'created_at' => wc_api_prepare_date_response( $customer->user_registered ), - 'updated_at' => $customer->last_update ? wc_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, + 'created_at' => wc_rest_api_prepare_date_response( $customer->user_registered ), + 'updated_at' => $customer->last_update ? wc_rest_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'last_order' => array( 'id' => is_object( $last_order ) ? $last_order->id : null, - 'date' => is_object( $last_order ) ? wc_api_prepare_date_response( $last_order->post->post_date_gmt ) : null + 'date' => is_object( $last_order ) ? wc_rest_api_prepare_date_response( $last_order->post->post_date_gmt ) : null ), 'orders_count' => wc_get_customer_order_count( $customer->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), diff --git a/includes/wc-api-functions.php b/includes/wc-api-functions.php index 4ca5229944d..465dd8d3036 100644 --- a/includes/wc-api-functions.php +++ b/includes/wc-api-functions.php @@ -20,11 +20,12 @@ if ( ! defined( 'ABSPATH' ) ) { * Requered WP 4.4 or later. * See https://developer.wordpress.org/reference/functions/mysql_to_rfc3339/ * + * @since 2.6.0 * @param string $date_gmt * @param string|null $date * @return string|null ISO8601/RFC3339 formatted datetime. */ -function wc_api_prepare_date_response( $date_gmt, $date = null ) { +function wc_rest_api_prepare_date_response( $date_gmt, $date = null ) { // Check if mysql_to_rfc3339 exists first! if ( ! function_exists( 'mysql_to_rfc3339' ) ) { return null; @@ -43,3 +44,105 @@ function wc_api_prepare_date_response( $date_gmt, $date = null ) { // Return the formatted datetime. return mysql_to_rfc3339( $date_gmt ); } + +/** + * Upload image from URL. + * + * @since 2.6.0 + * @param string $image_url + * @return array|WP_Error Attachment data or error message. + */ +function wc_rest_api_upload_image_from_url( $image_url ) { + $file_name = basename( current( explode( '?', $image_url ) ) ); + $wp_filetype = wp_check_filetype( $file_name, null ); + $parsed_url = @parse_url( $image_url ); + + // Check parsed URL. + if ( ! $parsed_url || ! is_array( $parsed_url ) ) { + return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + } + + // Ensure url is valid. + $image_url = str_replace( ' ', '%20', $image_url ); + + // Get the file. + $response = wp_safe_remote_get( $image_url, array( + 'timeout' => 10 + ) ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + return new WP_Error( 'woocommerce_rest_invalid_remote_image_url', sprintf( __( 'Error getting remote image %s', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + } + + // Ensure we have a file name and type. + if ( ! $wp_filetype['type'] ) { + $headers = wp_remote_retrieve_headers( $response ); + if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { + $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); + $disposition = sanitize_file_name( $disposition ); + $file_name = $disposition; + } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { + $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); + } + unset( $headers ); + } + + // Upload the file. + $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); + + if ( $upload['error'] ) { + return new WP_Error( 'woocommerce_rest_image_upload_error', $upload['error'], array( 'status' => 400 ) ); + } + + // Get filesize. + $filesize = filesize( $upload['file'] ); + + if ( 0 == $filesize ) { + @unlink( $upload['file'] ); + unset( $upload ); + + return new WP_Error( 'woocommerce_rest_image_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), array( 'status' => 400 ) ); + } + + do_action( 'woocommerce_rest_api_uploaded_image_from_url', $upload, $image_url ); + + return $upload; +} + +/** + * Set uploaded image as attachment. + * + * @since 2.6.0 + * @param array $upload Upload information from wp_upload_bits. + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ +function wc_rest_api_set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = $image_meta['title']; + } + if ( trim( $image_meta['caption'] ) ) { + $content = $image_meta['caption']; + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; +} From a6df9969f4ab5e6db4a4555f487d749feabf06c9 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 14:39:42 -0300 Subject: [PATCH 036/177] Fixed image upload --- includes/wc-api-functions.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/wc-api-functions.php b/includes/wc-api-functions.php index 465dd8d3036..afa177750a2 100644 --- a/includes/wc-api-functions.php +++ b/includes/wc-api-functions.php @@ -122,7 +122,11 @@ function wc_rest_api_set_uploaded_image_as_attachment( $upload, $id = 0 ) { $title = ''; $content = ''; - if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { + include_once( ABSPATH . 'wp-admin/includes/image.php' ); + } + + if ( $image_meta = wp_read_image_metadata( $upload['file'] ) ) { if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = $image_meta['title']; } From b5e65c68b96ad36efab3956290ce49d0c9c35aab Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 14:49:14 -0300 Subject: [PATCH 037/177] Added methods to update and create terms --- .../abstract-wc-rest-terms-controller.php | 186 ++++++++++++++++++ .../wc-rest-product-categories-controller.php | 31 +++ 2 files changed, 217 insertions(+) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 6395d35c291..fa8a2f25bbf 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -98,6 +98,22 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access to create a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + + if ( ! current_user_can( $taxonomy->cap->manage_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read a term. * @@ -108,6 +124,26 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return $this->get_items_permissions_check( $request ); } + /** + * Check if a given request has access to update a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $term = get_term( (int) $request['id'], $this->taxonomy ); + if ( ! $term ) { + return new WP_Error( "woocommerce_rest_{$this->taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy = get_taxonomy( $this->taxonomy ); + if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to delete a term. * @@ -236,6 +272,78 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return $response; } + /** + * Create a single term for a taxonomy. + * + * @param WP_REST_Request $request Full details about the request + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $name = $request['name']; + $args = array(); + + if ( isset( $request['description'] ) ) { + $args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $args['slug'] = $request['slug']; + } + + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $parent = get_term( (int) $request['parent'], $this->taxonomy ); + + if ( ! $parent ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args['parent'] = $parent->term_id; + } + + $term = wp_insert_term( $name, $this->taxonomy, $args ); + if ( is_wp_error( $term ) ) { + + // If we're going to inform the client that the term exists, give them the identifier + // they can actually use. + if ( ( $term_id = $term->get_error_data( 'term_exists' ) ) ) { + $existing_term = get_term( $term_id, $this->taxonomy ); + $term->add_data( $existing_term->term_id, 'term_exists' ); + } + + return $term; + } + + $term = get_term( $term['term_id'], $this->taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Add term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->taxonomy}", $term, $request, true ); + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $term, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base . '/' . $term->term_id ) ); + + return $response; + } + /** * Get a single term from a taxonomy. * @@ -258,6 +366,73 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return rest_ensure_response( $response ); } + /** + * Update a single term from a taxonomy + * + * @param WP_REST_Request $request Full details about the request + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + + $prepared_args = array(); + if ( isset( $request['name'] ) ) { + $prepared_args['name'] = $request['name']; + } + if ( isset( $request['description'] ) ) { + $prepared_args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $prepared_args['slug'] = $request['slug']; + } + + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $parent = get_term( (int) $request['parent'], $this->taxonomy ); + + if ( ! $parent ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_args['parent'] = $parent->term_id; + } + + $term = get_term( (int) $request['id'], $this->taxonomy ); + + // Only update the term if we haz something to update. + if ( ! empty( $prepared_args ) ) { + $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); + if ( is_wp_error( $update ) ) { + return $update; + } + } + + $term = get_term( (int) $request['id'], $this->taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Update term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->taxonomy}", $term, $request, false ); + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $term, $request ); + return rest_ensure_response( $response ); + } + /** * Delete a single term from a taxonomy. * @@ -322,6 +497,17 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return $links; } + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + return true; + } + /** * Get the terms attached to a product. * diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index ee248aa88cc..68a73555a13 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -84,6 +84,36 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); } + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + update_woocommerce_term_meta( $id, 'display_type', $request['display'] ); + + if ( ! empty( $request['image'] ) ) { + $upload = wc_rest_api_upload_image_from_url( esc_url_raw( $request['image'] ) ); + + if ( is_wp_error( $upload ) ) { + return $upload; + } + + $image_id = wc_rest_api_set_uploaded_image_as_attachment( $upload ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_woocommerce_term_meta( $id, 'thumbnail_id', $image_id ); + } + } + + return true; + } + /** * Get the Term's schema, conforming to JSON Schema. * @@ -133,6 +163,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { 'display' => array( 'description' => __( 'Category archive display type.', 'woocommerce' ), 'type' => 'string', + 'default' => 'default', 'enum' => array( 'default', 'products', 'subcategories', 'both' ), 'context' => array( 'view', 'edit' ), ), From 79cf96c07b741a52f27bf224f01f64136ce222fb Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 15:22:17 -0300 Subject: [PATCH 038/177] Updated rest controller --- includes/vendor/class-wp-rest-controller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/vendor/class-wp-rest-controller.php b/includes/vendor/class-wp-rest-controller.php index 9633a039a87..4d54072c36e 100644 --- a/includes/vendor/class-wp-rest-controller.php +++ b/includes/vendor/class-wp-rest-controller.php @@ -196,7 +196,9 @@ abstract class WP_REST_Controller { continue; } if ( ! in_array( $context, $details['context'] ) ) { - unset( $data[ $key ][ $attribute ] ); + if ( isset( $data[ $key ][ $attribute ] ) ) { + unset( $data[ $key ][ $attribute ] ); + } } } } From 9f07be5271df8d46493c6d8636e5665f4cbdde5d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 15:36:17 -0300 Subject: [PATCH 039/177] Make easy to extend our rest controllers --- .../abstract-wc-rest-posts-controller.php | 11 +++++++-- .../abstract-wc-rest-terms-controller.php | 10 ++++---- includes/api/wc-rest-coupons-controller.php | 11 +++++++-- includes/api/wc-rest-customers-controller.php | 23 ++++++++++++------- .../api/wc-rest-order-notes-controller.php | 7 ++++++ .../api/wc-rest-order-refunds-controller.php | 9 +++++++- includes/api/wc-rest-orders-controller.php | 9 +++++++- ...est-product-attribute-terms-controller.php | 7 ++++++ .../wc-rest-product-attributes-controller.php | 7 ++++++ .../wc-rest-product-categories-controller.php | 7 ++++++ ...st-product-shipping-classes-controller.php | 7 ++++++ .../api/wc-rest-product-tags-controller.php | 7 ++++++ includes/api/wc-rest-products-controller.php | 9 +++++++- includes/api/wc-rest-reports-controller.php | 7 ++++++ .../api/wc-rest-tax-classes-controller.php | 7 ++++++ includes/api/wc-rest-taxes-controller.php | 7 ++++++ includes/class-wc-api.php | 5 ---- 17 files changed, 125 insertions(+), 25 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index c53288f8b2d..342241b70c2 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -14,6 +14,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -246,7 +253,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $response = $this->prepare_item_for_response( $post, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $post_id ) ) ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); return $response; } @@ -431,7 +438,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { unset( $request_params['filter']['posts_per_page'] ); unset( $request_params['filter']['paged'] ); } - $base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ) ); + $base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index fa8a2f25bbf..e5dc1e6a366 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -32,7 +32,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * Register the routes for terms. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), @@ -52,7 +52,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), )); - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), @@ -254,7 +254,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $max_pages = ceil( $total_terms / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); - $base = add_query_arg( $request->get_query_params(), rest_url( '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base ) ); + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $this->rest_base ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { @@ -339,7 +339,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $response = $this->prepare_item_for_response( $term, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); - $response->header( 'Location', rest_url( '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base . '/' . $term->term_id ) ); + $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) ); return $response; } @@ -475,7 +475,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return array Links for the given term. */ protected function prepare_links( $term ) { - $base = '/' . WC_API::REST_API_NAMESPACE . '/' . $this->rest_base; + $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 6ebc0255052..5d77831f1db 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -40,7 +47,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * Register the routes for coupons. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), @@ -60,7 +67,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index b6ad77074be..e37260e3f80 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Customers_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -33,7 +40,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * Register the routes for customers. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base, array( + register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), @@ -59,7 +66,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), @@ -89,7 +96,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/me', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_current_item' ), 'args' => array( @@ -263,7 +270,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $max_pages = ceil( $total_users / $per_page ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); - $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ) ); + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { $prev_page = $page - 1; if ( $prev_page > $max_pages ) { @@ -324,7 +331,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $response = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $customer_id ) ) ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer_id ) ) ); return $response; } @@ -467,7 +474,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $customer = wp_get_current_user(); $response = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $response ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $current_customer_id ) ) ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $current_customer_id ) ) ); $response->set_status( 302 ); return $response; @@ -590,10 +597,10 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { protected function prepare_links( $customer ) { $links = array( 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $customer->ID ) ), + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), ), 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', WC_API::REST_API_NAMESPACE, $this->rest_base ) ), + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), ), ); diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index b7baa5f143f..9304a130d6a 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php index 984818837ba..554ad691609 100644 --- a/includes/api/wc-rest-order-refunds-controller.php +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -40,7 +47,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { * Register the routes for order refunds. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 59f6ea95b94..507414a2bc4 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -40,7 +47,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * Register the routes for orders. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 21cf5f03a06..66e0ff0a1f6 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index dd463281385..86492eaa028 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index 68a73555a13..43ee2335b63 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index 66e14638fc4..dd657150f43 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 86b2f760fa7..8f5a3641099 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/wc-rest-products-controller.php index 185d980086b..5413b59b328 100644 --- a/includes/api/wc-rest-products-controller.php +++ b/includes/api/wc-rest-products-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * @@ -40,7 +47,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { * Register the routes for products. */ public function register_routes() { - register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index dcf31ffc744..599e5678eaf 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Reports_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 8fb40940966..b96427f41db 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index cb14cfb9803..f216497b5f5 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -22,6 +22,13 @@ if ( ! defined( 'ABSPATH' ) ) { */ class WC_REST_Taxes_Controller extends WP_REST_Controller { + /** + * Endpoint namespace. + * + * @var string + */ + public $namepsace = 'wc/v1'; + /** * Route base. * diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 062e308276b..8178aceb227 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -18,11 +18,6 @@ if ( ! class_exists( 'WC_API' ) ) : class WC_API { - /** - * WP REST API namespace/version. - */ - const REST_API_NAMESPACE = 'wc/v1'; - /** * This is the major version for the REST API and takes * first-order position in endpoint URLs. From 349b817d1e1215ccd678edbc9f9bf6381827aa3a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 15:45:10 -0300 Subject: [PATCH 040/177] Fixed typo --- includes/abstracts/abstract-wc-rest-posts-controller.php | 2 +- includes/api/wc-rest-coupons-controller.php | 2 +- includes/api/wc-rest-customers-controller.php | 2 +- includes/api/wc-rest-order-notes-controller.php | 2 +- includes/api/wc-rest-order-refunds-controller.php | 2 +- includes/api/wc-rest-orders-controller.php | 2 +- .../api/wc-rest-product-attribute-terms-controller.php | 8 +------- includes/api/wc-rest-product-attributes-controller.php | 2 +- includes/api/wc-rest-product-categories-controller.php | 2 +- .../api/wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/wc-rest-product-tags-controller.php | 2 +- includes/api/wc-rest-products-controller.php | 2 +- includes/api/wc-rest-reports-controller.php | 2 +- includes/api/wc-rest-tax-classes-controller.php | 2 +- includes/api/wc-rest-taxes-controller.php | 2 +- 15 files changed, 15 insertions(+), 21 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 342241b70c2..eb021065bd0 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -19,7 +19,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 5d77831f1db..e4d9c20cc16 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -27,7 +27,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index e37260e3f80..30fa2aecda8 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -27,7 +27,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 9304a130d6a..eb8e12b1bd0 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/wc-rest-order-refunds-controller.php index 554ad691609..9894ce379b6 100644 --- a/includes/api/wc-rest-order-refunds-controller.php +++ b/includes/api/wc-rest-order-refunds-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 507414a2bc4..e301e75e401 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -27,7 +27,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 66e0ff0a1f6..ea851f39451 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. @@ -36,10 +36,4 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle */ protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; - /** - * Register the routes for product attribute terms. - */ - public function register_routes() { - - } } diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 86492eaa028..ac68913be0e 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index 43ee2335b63..65d19df34ff 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index dd657150f43..56da0749bc1 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 8f5a3641099..7c8e14f39a3 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/wc-rest-products-controller.php index 5413b59b328..81b2ae51667 100644 --- a/includes/api/wc-rest-products-controller.php +++ b/includes/api/wc-rest-products-controller.php @@ -27,7 +27,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index 599e5678eaf..b46d8c642f5 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -27,7 +27,7 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index b96427f41db..254496c4655 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index f216497b5f5..f30cec8ac89 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * * @var string */ - public $namepsace = 'wc/v1'; + public $namespace = 'wc/v1'; /** * Route base. From 426410c2aec862966bfb99afba6f1c8e0fee1aab Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 16:39:24 -0300 Subject: [PATCH 041/177] Manipule product attribute terms --- .../abstract-wc-rest-terms-controller.php | 157 ++++++++++++------ ...est-product-attribute-terms-controller.php | 79 +++++++++ 2 files changed, 189 insertions(+), 47 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index e5dc1e6a366..6b1e5844a3e 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -89,9 +89,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - $taxonomy = get_taxonomy( $this->taxonomy ); + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } - if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy->cap->edit_terms ) ) { + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -105,9 +109,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { - $taxonomy = get_taxonomy( $this->taxonomy ); + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } - if ( ! current_user_can( $taxonomy->cap->manage_terms ) ) { + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -131,13 +139,18 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { - $term = get_term( (int) $request['id'], $this->taxonomy ); - if ( ! $term ) { - return new WP_Error( "woocommerce_rest_{$this->taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy = get_taxonomy( $this->taxonomy ); - if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) { + $term = get_term( (int) $request['id'], $taxonomy ); + if ( ! $term ) { + return new WP_Error( "woocommerce_rest_{$taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -151,13 +164,18 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { - $term = get_term( (int) $request['id'], $this->taxonomy ); - if ( ! $term ) { - return new WP_Error( "woocommerce_rest_{$this->taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy = get_taxonomy( $this->taxonomy ); - if ( ! current_user_can( $taxonomy->cap->delete_terms ) ) { + $term = get_term( (int) $request['id'], $taxonomy ); + if ( ! $term ) { + return new WP_Error( "woocommerce_rest_{$taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -167,10 +185,12 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { /** * Get terms associated with a taxonomy. * - * @param WP_REST_Request $request Full details about the request + * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $prepared_args = array( 'exclude' => $request['exclude'], 'include' => $request['include'], @@ -189,9 +209,9 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; } - $taxonomy = get_taxonomy( $this->taxonomy ); + $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( $taxonomy->hierarchical && isset( $request['parent'] ) ) { + if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { if ( 0 === $request['parent'] ) { // Only query top-level terms. $prepared_args['parent'] = 0; @@ -214,18 +234,18 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * passed to get_terms. * @param WP_REST_Request $request The current request. */ - $prepared_args = apply_filters( "woocommerce_rest_{$this->taxonomy}_query", $prepared_args, $request ); + $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); if ( ! empty( $prepared_args['product'] ) ) { $query_result = $this->get_terms_for_product( $prepared_args ); $total_terms = $this->total_terms; } else { - $query_result = get_terms( $this->taxonomy, $prepared_args ); + $query_result = get_terms( $taxonomy, $prepared_args ); $count_args = $prepared_args; unset( $count_args['number'] ); unset( $count_args['offset'] ); - $total_terms = wp_count_terms( $this->taxonomy, $count_args ); + $total_terms = wp_count_terms( $taxonomy, $count_args ); // Ensure we don't return results when offset is out of bounds. // See https://core.trac.wordpress.org/ticket/35935 @@ -275,14 +295,16 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { /** * Create a single term for a taxonomy. * - * @param WP_REST_Request $request Full details about the request + * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function create_item( $request ) { - $name = $request['name']; - $args = array(); + $taxonomy = $this->get_taxonomy( $request ); + $name = $request['name']; + $args = array(); - if ( isset( $request['description'] ) ) { + $schema = $this->get_item_schema(); + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $args['description'] = $request['description']; } if ( isset( $request['slug'] ) ) { @@ -290,11 +312,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { } if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); } - $parent = get_term( (int) $request['parent'], $this->taxonomy ); + $parent = get_term( (int) $request['parent'], $taxonomy ); if ( ! $parent ) { return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); @@ -303,20 +325,20 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $args['parent'] = $parent->term_id; } - $term = wp_insert_term( $name, $this->taxonomy, $args ); + $term = wp_insert_term( $name, $taxonomy, $args ); if ( is_wp_error( $term ) ) { // If we're going to inform the client that the term exists, give them the identifier // they can actually use. if ( ( $term_id = $term->get_error_data( 'term_exists' ) ) ) { - $existing_term = get_term( $term_id, $this->taxonomy ); + $existing_term = get_term( $term_id, $taxonomy ); $term->add_data( $existing_term->term_id, 'term_exists' ); } return $term; } - $term = get_term( $term['term_id'], $this->taxonomy ); + $term = get_term( $term['term_id'], $taxonomy ); $this->update_additional_fields_for_object( $term, $request ); @@ -333,7 +355,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating term, false when updating. */ - do_action( "woocommerce_rest_insert_{$this->taxonomy}", $term, $request, true ); + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); $request->set_param( 'context', 'view' ); $response = $this->prepare_item_for_response( $term, $request ); @@ -351,9 +373,10 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_REST_Request|WP_Error */ public function get_item( $request ) { - $term = get_term( (int) $request['id'], $this->taxonomy ); + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); - if ( ! $term || $term->taxonomy !== $this->taxonomy ) { + if ( ! $term || $term->taxonomy !== $taxonomy ) { return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } @@ -367,18 +390,20 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { } /** - * Update a single term from a taxonomy + * Update a single term from a taxonomy. * - * @param WP_REST_Request $request Full details about the request + * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $schema = $this->get_item_schema(); $prepared_args = array(); if ( isset( $request['name'] ) ) { $prepared_args['name'] = $request['name']; } - if ( isset( $request['description'] ) ) { + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $prepared_args['description'] = $request['description']; } if ( isset( $request['slug'] ) ) { @@ -386,11 +411,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { } if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); } - $parent = get_term( (int) $request['parent'], $this->taxonomy ); + $parent = get_term( (int) $request['parent'], $taxonomy ); if ( ! $parent ) { return new WP_Error( 'woocommerce_rest_term_invalid', __( "Parent resource doesn't exist.", 'woocommerce' ), array( 'status' => 400 ) ); @@ -399,7 +424,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $prepared_args['parent'] = $parent->term_id; } - $term = get_term( (int) $request['id'], $this->taxonomy ); + $term = get_term( (int) $request['id'], $taxonomy ); // Only update the term if we haz something to update. if ( ! empty( $prepared_args ) ) { @@ -409,7 +434,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { } } - $term = get_term( (int) $request['id'], $this->taxonomy ); + $term = get_term( (int) $request['id'], $taxonomy ); $this->update_additional_fields_for_object( $term, $request ); @@ -426,7 +451,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating term, false when updating. */ - do_action( "woocommerce_rest_insert_{$this->taxonomy}", $term, $request, false ); + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); $request->set_param( 'context', 'view' ); $response = $this->prepare_item_for_response( $term, $request ); @@ -436,18 +461,19 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { /** * Delete a single term from a taxonomy. * - * @param WP_REST_Request $request Full details about the request + * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + $taxonomy = $this->get_taxonomy( $request ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); } - $term = get_term( (int) $request['id'], $this->taxonomy ); + $term = get_term( (int) $request['id'], $taxonomy ); $request->set_param( 'context', 'view' ); $response = $this->prepare_item_for_response( $term, $request ); @@ -463,7 +489,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ - do_action( "woocommerce_rest_delete_{$this->taxonomy}", $term, $response, $request ); + do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); return $response; } @@ -472,10 +498,16 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * Prepare links for the request. * * @param object $term Term object. + * @param WP_REST_Request $request Full details about the request. * @return array Links for the given term. */ - protected function prepare_links( $term ) { + protected function prepare_links( $term, $request ) { $base = '/' . $this->namespace . '/' . $this->rest_base; + + if ( $request['attribute_id'] ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), @@ -520,7 +552,9 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return array List of term objects. (Total count in `$this->total_terms`). */ protected function get_terms_for_product( $prepared_args ) { - $query_result = get_the_terms( $prepared_args['product'], $this->taxonomy ); + $taxonomy = $this->get_taxonomy( $request ); + + $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); if ( empty( $query_result ) ) { $this->total_terms = 0; return array(); @@ -582,7 +616,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { */ public function get_collection_params() { $query_params = parent::get_collection_params(); - $taxonomy = get_taxonomy( $this->taxonomy ); + + if ( '' !== $this->taxonomy ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + } else { + $taxonomy = new stdClass(); + $taxonomy->hierarchical = true; + } $query_params['context']['default'] = 'view'; @@ -658,6 +698,29 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); + return $query_params; } + + /** + * Get taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function get_taxonomy( $request ) { + // Check if taxonomy is defined. + // Prevents check for attribute taxonomy more than one time for each query. + if ( '' !== $this->taxonomy ) { + return $this->taxonomy; + } + + if ( $request['attribute_id'] ) { + $taxonomy = wc_attribute_taxonomy_name_by_id( (int) $request['attribute_id'] ); + + $this->taxonomy = $taxonomy; + } + + return $this->taxonomy; + } } diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index ea851f39451..7a404eefd38 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -36,4 +36,83 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle */ protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; + /** + * Prepare a single product tag output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'woocommerce' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } } From ff6970031baec72a635fda1186aab0bcb1fb0478 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 17:04:32 -0300 Subject: [PATCH 042/177] Fixed attribute terms schema title --- includes/api/wc-rest-product-attribute-terms-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 7a404eefd38..20ddd8771e4 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -79,7 +79,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->taxonomy, + 'title' => 'product_attribute_term', 'type' => 'object', 'properties' => array( 'id' => array( From 396242b89ce1c8b5e022554d242e378b509a53b0 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 17:28:52 -0300 Subject: [PATCH 043/177] Fixed links for terms --- includes/abstracts/abstract-wc-rest-terms-controller.php | 4 ++-- includes/api/wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/wc-rest-product-categories-controller.php | 2 +- includes/api/wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/wc-rest-product-tags-controller.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 6b1e5844a3e..27f917bb486 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -504,7 +504,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { protected function prepare_links( $term, $request ) { $base = '/' . $this->namespace . '/' . $this->rest_base; - if ( $request['attribute_id'] ) { + if ( ! empty( $request['attribute_id'] ) ) { $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); } @@ -715,7 +715,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { return $this->taxonomy; } - if ( $request['attribute_id'] ) { + if ( ! empty( $request['attribute_id'] ) ) { $taxonomy = wc_attribute_taxonomy_name_by_id( (int) $request['attribute_id'] ); $this->taxonomy = $taxonomy; diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 20ddd8771e4..2f603893fed 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -37,7 +37,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; /** - * Prepare a single product tag output for response. + * Prepare a single product attribute term output for response. * * @param obj $item Term object. * @param WP_REST_Request $request diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index 65d19df34ff..ccb78ccfa06 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -77,7 +77,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $item ) ); + $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index 56da0749bc1..a35430a8194 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -91,7 +91,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $item ) ); + $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 7c8e14f39a3..9096e0be867 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -65,7 +65,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $item ) ); + $response->add_links( $this->prepare_links( $item, $request ) ); /** * Filter a term item returned from the API. From c7ace7749f9c43318216c33fcca85bae47acea24 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 17:32:25 -0300 Subject: [PATCH 044/177] Created schema for product attributes and allow fetch product attributes by ID --- .../wc-rest-product-attributes-controller.php | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index ac68913be0e..5fae3050142 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -36,10 +36,315 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { */ protected $rest_base = 'products/attributes'; + /** + * Attribute name. + * + * @var string + */ + protected $attribute = ''; + /** * Register the routes for product attributes. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + return $this->get_items_permissions_check( $request ); + } + + /** + * Check if a given request has access to update a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + global $wpdb; + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $request['id'] ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + return new WP_Error( 'woocommerce_rest_attribute_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $response = $this->prepare_item_for_response( $attribute, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare a single product attribute output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->attribute_id, + 'name' => $item->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), + 'type' => $item->attribute_type, + 'order_by' => $item->attribute_orderby, + 'has_archives' => (bool) $item->attribute_public, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the product attribute data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $attribute Attribute object. + * @param WP_REST_Request $request Full details about the request. + * @return array Links for the given attribute. + */ + protected function prepare_links( $attribute ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_attribute', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'type' => array( + 'description' => __( 'Type of attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'select', + 'enum' => array_keys( wc_get_attribute_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'order_by' => array( + 'description' => __( 'Default sort order.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'menu_order', + 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ), + 'context' => array( 'view', 'edit' ), + ), + 'has_archives' => array( + 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get attribute name. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function get_taxonomy( $request ) { + if ( '' !== $this->attribute ) { + return $this->attribute; + } + + if ( $request['id'] ) { + $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); + + $this->attribute = $name; + } + + return $this->attribute; } } From 3c770e1bc4ceed14bd7160f5a272d8b1426b5157 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 19:10:03 -0300 Subject: [PATCH 045/177] Added method to list product attributes --- .../wc-rest-product-attributes-controller.php | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 5fae3050142..c23a5695766 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -104,13 +104,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'manage_product_terms' ) ) { return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -188,7 +182,26 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Get a single term from a taxonomy. + * Get all attributes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $attributes = wc_get_attribute_taxonomies(); + + $data = array(); + foreach ( $attributes as $attribute_obj ) { + $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); + $attribute = $this->prepare_response_for_collection( $attribute ); + $data[] = $attribute; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Request|WP_Error @@ -196,14 +209,10 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { public function get_item( $request ) { global $wpdb; - $attribute = $wpdb->get_row( $wpdb->prepare( " - SELECT * - FROM {$wpdb->prefix}woocommerce_attribute_taxonomies - WHERE attribute_id = %d - ", $request['id'] ) ); + $attribute = $this->get_attribute( $request['id'] ); - if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { - return new WP_Error( 'woocommerce_rest_attribute_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + if ( is_wp_error( $attribute ) ) { + return $attribute; } $response = $this->prepare_item_for_response( $attribute, $request ); @@ -328,6 +337,18 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $new_params = array(); + $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + + return $new_params; + } + /** * Get attribute name. * @@ -347,4 +368,24 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return $this->attribute; } + + /** + * Get attribute data. + * + * @param int $id Attribute ID. + * @return stdClass|WP_Error + */ + protected function get_attribute( $id ) { + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + return new WP_Error( 'woocommerce_rest_attribute_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $attribute; + } } From 6eab81a79f8b01235a18165f2db1724d5fe8567a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 19:36:34 -0300 Subject: [PATCH 046/177] Added method to create new attributes --- .../wc-rest-product-attributes-controller.php | 114 +++++++++++++++--- 1 file changed, 100 insertions(+), 14 deletions(-) diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index c23a5695766..9c5475405f1 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -98,7 +98,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to read the terms. + * Check if a given request has access to read the attributes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -112,19 +112,13 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to create a term. + * Check if a given request has access to create a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) { + if ( ! current_user_can( 'manage_product_terms' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -132,7 +126,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to read a term. + * Check if a given request has access to read a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -142,7 +136,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to update a term. + * Check if a given request has access to update a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -162,7 +156,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access to delete a term. + * Check if a given request has access to delete a attribute. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -200,6 +194,76 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return rest_ensure_response( $data ); } + /** + * Create a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + global $wpdb; + + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + // Set the attribute slug. + if ( empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = wc_sanitize_taxonomy_name( stripslashes( $args['attribute_label'] ) ); + } else { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + } + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error. + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_product_attribute', $insert->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $wpdb->insert_id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( "woocommerce_rest_insert_product_attribute", $attribute, $request, true ); + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + /** * Get a single attribute. * @@ -246,12 +310,12 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { $response->add_links( $this->prepare_links( $item ) ); /** - * Filter a term item returned from the API. + * Filter a attribute item returned from the API. * * Allows modification of the product attribute data right before it is returned. * * @param WP_REST_Response $response The response object. - * @param object $item The original term object. + * @param object $item The original attribute object. * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); @@ -329,6 +393,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { 'has_archives' => array( 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), 'type' => 'boolean', + 'default' => false, 'context' => array( 'view', 'edit' ), ), ), @@ -376,6 +441,8 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return stdClass|WP_Error */ protected function get_attribute( $id ) { + global $wpdb; + $attribute = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies @@ -388,4 +455,23 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return $attribute; } + + /** + * Validate attribute slug. + * + * @param string $slug + * @param bool $new_data + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max).', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } else if ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } else if ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + return true; + } } From 578870c13328495ad7493ce6f96f63813c28d387 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 19:57:24 -0300 Subject: [PATCH 047/177] Added method to edit attributes --- .../wc-rest-product-attributes-controller.php | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 9c5475405f1..de131b26490 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -229,7 +229,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { array( '%s', '%s', '%s', '%s', '%d' ) ); - // Checks for an error. + // Checks for errors. if ( is_wp_error( $insert ) ) { return new WP_Error( 'woocommerce_rest_cannot_create_product_attribute', $insert->get_error_message(), array( 'status' => 400 ) ); } @@ -249,7 +249,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @param WP_REST_Request $request Request object. * @param boolean $creating True when creating attribute, false when updating. */ - do_action( "woocommerce_rest_insert_product_attribute", $attribute, $request, true ); + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); $request->set_param( 'context', 'view' ); $response = $this->prepare_item_for_response( $attribute, $request ); @@ -284,6 +284,85 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return rest_ensure_response( $response ); } + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + $i = 0; + foreach ( $args as $key => $value ) { + if ( empty( $value ) && ! is_bool( $value ) ) { + unset( $args[ $key ] ); + unset( $format[ $i ] ); + } + + $i++; + } + + // Set the attribute slug. + if ( ! empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + } + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + // Checks for errors. + if ( false === $update ) { + return new WP_Error( 'woocommerce_rest_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return rest_ensure_response( $response ); + } + /** * Prepare a single product attribute output for response. * From c27c9d38654981d444a1a39491a5b257411ad2b7 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 7 Mar 2016 20:12:51 -0300 Subject: [PATCH 048/177] Added method to delete attributes --- .../wc-rest-product-attributes-controller.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index de131b26490..62b5008a9aa 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -363,6 +363,69 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return rest_ensure_response( $response ); } + /** + * Delete a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + global $wpdb; + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $attribute = $this->get_attribute( $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $attribute->attribute_id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute->attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * Fires after a single attribute is deleted via the REST API. + * + * @param stdObject $attribute The deleted attribute. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); + + // Fires woocommerce_attribute_deleted hook. + do_action( 'woocommerce_attribute_deleted', $attribute->attribute_id, $attribute->attribute_name, $taxonomy ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + /** * Prepare a single product attribute output for response. * From e395cebfaf437e7d5c7a1557f2edfc2faa5d8e72 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 8 Mar 2016 19:51:36 -0300 Subject: [PATCH 049/177] Created taxes schema --- includes/api/wc-rest-taxes-controller.php | 127 ++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index f30cec8ac89..74208d2a323 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -40,6 +40,133 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * Register the routes for coupons. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get the User's schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode/ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tax rate name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'priority' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + ), + 'compound' => array( + 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'shipping' => array( + 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From dcca3a1efe45e4660fcf565de97cd0ea5e429fe1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 01:11:56 -0300 Subject: [PATCH 050/177] Allow get tax as object --- includes/class-wc-tax.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index 0085ab37480..3f6144db8b7 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -727,18 +727,19 @@ class WC_Tax { * @since 2.5.0 * @access private * - * @param int $tax_rate_id + * @param int $tax_rate_id + * @param string $output_type * * @return array */ - public static function _get_tax_rate( $tax_rate_id ) { + public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d - ", $tax_rate_id ), ARRAY_A ); + ", $tax_rate_id ), $output_type ); } /** From 4f1551ff5bcc2859899553907403d0f861a151aa Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 01:12:21 -0300 Subject: [PATCH 051/177] Added method to get single taxes --- includes/api/wc-rest-customers-controller.php | 2 +- includes/api/wc-rest-taxes-controller.php | 173 +++++++++++++++++- 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 30fa2aecda8..99e41e359a7 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -831,7 +831,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } /** - * Get the query params for collections + * Get the query params for collections. * * @return array */ diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 74208d2a323..723517b87bc 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -87,6 +87,117 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { ) ); } + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_tax_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_tax_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->prepare_item_for_response( $tax_obj, $request ); + $response = rest_ensure_response( $tax ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + global $wpdb; + + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + // Get locales from a tax rate. + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given user. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + /** * Get the User's schema, conforming to JSON Schema * @@ -95,7 +206,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'customer', + 'title' => 'tax', 'type' => 'object', 'properties' => array( 'id' => array( @@ -169,4 +280,64 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'view'; + + $query_params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $query_params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $query_params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $query_params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } } From 5f73c23da6b6fde4ffb5eaaffc2a60e337a7c5c6 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 01:46:04 -0300 Subject: [PATCH 052/177] Better query params name --- .../abstract-wc-rest-terms-controller.php | 24 +++++++++---------- includes/api/wc-rest-customers-controller.php | 18 +++++++------- .../wc-rest-product-attributes-controller.php | 6 ++--- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 27f917bb486..3f45c4f3ed7 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -615,7 +615,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return array */ public function get_collection_params() { - $query_params = parent::get_collection_params(); + $params = parent::get_collection_params(); if ( '' !== $this->taxonomy ) { $taxonomy = get_taxonomy( $this->taxonomy ); @@ -624,29 +624,29 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $taxonomy->hierarchical = true; } - $query_params['context']['default'] = 'view'; + $params['context']['default'] = 'view'; - $query_params['exclude'] = array( + $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $query_params['include'] = array( + $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); if ( ! $taxonomy->hierarchical ) { - $query_params['offset'] = array( + $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); } - $query_params['order'] = array( + $params['order'] = array( 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', @@ -657,7 +657,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { ), 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['orderby'] = array( + $params['orderby'] = array( 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', @@ -673,33 +673,33 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { ), 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['hide_empty'] = array( + $params['hide_empty'] = array( 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'validate_callback' => 'rest_validate_request_arg', ); if ( $taxonomy->hierarchical ) { - $query_params['parent'] = array( + $params['parent'] = array( 'description' => __( 'Limit result set to resources assigned to a specific parent.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); } - $query_params['product'] = array( + $params['product'] = array( 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), 'type' => 'integer', 'default' => null, 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['slug'] = array( + $params['slug'] = array( 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - return $query_params; + return $params; } /** diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 99e41e359a7..8283b3959d7 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -836,29 +836,29 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return array */ public function get_collection_params() { - $query_params = parent::get_collection_params(); + $params = parent::get_collection_params(); - $query_params['context']['default'] = 'view'; + $params['context']['default'] = 'view'; - $query_params['exclude'] = array( + $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $query_params['include'] = array( + $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $query_params['offset'] = array( + $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['order'] = array( + $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), @@ -866,7 +866,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['orderby'] = array( + $params['orderby'] = array( 'default' => 'name', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( @@ -879,11 +879,11 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['slug'] = array( + $params['slug'] = array( 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - return $query_params; + return $params; } } diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 62b5008a9aa..35578c52bdf 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -550,10 +550,10 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return array */ public function get_collection_params() { - $new_params = array(); - $new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); - return $new_params; + return $params; } /** From 0817a854e87b4c577d698299a6e8cb01f2f2c888 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 01:51:36 -0300 Subject: [PATCH 053/177] Added method to list taxes --- includes/api/wc-rest-taxes-controller.php | 134 +++++++++++++++++++--- 1 file changed, 120 insertions(+), 14 deletions(-) diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 723517b87bc..04036b1622e 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -87,6 +87,20 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { ) ); } + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list taxes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read a tax. * @@ -101,6 +115,100 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return true; } + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + global $wpdb; + + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class. + if ( ! empty( $prepared_args['class'] ) ) { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + $query .= " AND tax_rate_class = '$class'"; + } + + // Order tax rates. + $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); + + // Pagination. + $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); + + // Query taxes. + $results = $wpdb->get_results( $query . $order_by . $pagination ); + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + + // Calcule totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + /** * Get a single tax. * @@ -287,29 +395,29 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return array */ public function get_collection_params() { - $query_params = parent::get_collection_params(); + $params = parent::get_collection_params(); - $query_params['context']['default'] = 'view'; + $params['context']['default'] = 'view'; - $query_params['exclude'] = array( + $params['exclude'] = array( 'description' => __( 'Ensure result set excludes specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $query_params['include'] = array( + $params['include'] = array( 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), 'type' => 'array', 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $query_params['offset'] = array( + $params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['order'] = array( + $params['order'] = array( 'default' => 'asc', 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), 'enum' => array( 'asc', 'desc' ), @@ -317,27 +425,25 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['orderby'] = array( - 'default' => 'name', + $params['orderby'] = array( + 'default' => 'order', 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), 'enum' => array( 'id', - 'include', - 'name', - 'registered_date', + 'order', ), 'sanitize_callback' => 'sanitize_key', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $query_params['class'] = array( + $params['class'] = array( 'description' => __( 'Sort by tax class.', 'woocommerce' ), 'enum' => array_merge( array( 'standard' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), - 'sanitize_callback' => 'sanitize_key', + 'sanitize_callback' => 'sanitize_title', 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - return $query_params; + return $params; } } From 17f1aa7e62f490f97ddd681800c4731df64dc93a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 02:21:44 -0300 Subject: [PATCH 054/177] Added method to create taxes --- includes/api/wc-rest-taxes-controller.php | 85 ++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 04036b1622e..24a7ea521e8 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -101,6 +101,20 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access create taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_tax', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read a tax. * @@ -116,7 +130,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { } /** - * Get all customers. + * Get all taxes. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response @@ -209,6 +223,75 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return $response; } + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '', + 'tax_rate_name' => '', + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => 0, + 'tax_rate_class' => '', + ); + + foreach ( $data as $key => $value ) { + $new_key = str_replace( 'tax_rate_', '', $key ); + $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; + + if ( ! empty( $request[ $new_key ] ) ) { + if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { + $data[ $key ] = (int) $request[ $new_key ]; + } else { + $data[ $key ] = $request[ $new_key ]; + } + } + } + + // Create tax rate. + $id = WC_Tax::_insert_tax_rate( $data ); + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT );; + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) ); + + return $response; + } + /** * Get a single tax. * From a4c5500bde13c62fdcb2c8fcb4fb4891637a39e1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 03:34:41 -0300 Subject: [PATCH 055/177] Added method to edit taxes and fixed create tax method --- includes/api/wc-rest-customers-controller.php | 2 +- includes/api/wc-rest-taxes-controller.php | 128 ++++++++++++++---- 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 8283b3959d7..700eb1604de 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -357,7 +357,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } /** - * Update a single user + * Update a single user. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 24a7ea521e8..28f71b74584 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -129,6 +129,20 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get all taxes. * @@ -235,30 +249,17 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { } $data = array( - 'tax_rate_country' => '', - 'tax_rate_state' => '', - 'tax_rate' => '', - 'tax_rate_name' => '', - 'tax_rate_priority' => 1, - 'tax_rate_compound' => 0, - 'tax_rate_shipping' => 1, - 'tax_rate_order' => 0, - 'tax_rate_class' => '', + 'tax_rate_country' => $request['country'], + 'tax_rate_state' => $request['state'], + 'tax_rate' => $request['rate'], + 'tax_rate_name' => $request['name'], + 'tax_rate_priority' => (int) $request['priority'], + 'tax_rate_compound' => (int) $request['compound'], + 'tax_rate_shipping' => (int) $request['shipping'], + 'tax_rate_order' => (int) $request['order'], + 'tax_rate_class' => 'standard' !== $request['class'] ? $request['class'] : '', ); - foreach ( $data as $key => $value ) { - $new_key = str_replace( 'tax_rate_', '', $key ); - $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; - - if ( ! empty( $request[ $new_key ] ) ) { - if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { - $data[ $key ] = (int) $request[ $new_key ]; - } else { - $data[ $key ] = $request[ $new_key ]; - } - } - } - // Create tax rate. $id = WC_Tax::_insert_tax_rate( $data ); @@ -270,7 +271,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); } - $tax = WC_Tax::_get_tax_rate( $id, OBJECT );; + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); $this->update_additional_fields_for_object( $tax, $request ); @@ -312,6 +313,87 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return $response; } + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $current_tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $current_tax ) ) { + return new WP_Error( 'woocommerce_rest_tax_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = array(); + $fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class' + ); + + foreach ( $fields as $field ) { + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + $value = $request[ $key ]; + + // Fix compund and shipping values. + if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { + $value = (int) $request[ $key ]; + } + + // Test new data against current data. + if ( $current_tax->$field === $value ) { + continue; + } + + $data[ $field ] = $request[ $key ]; + } + + // Update tax rate + WC_Tax::_update_tax_rate( $id, $data ); + + // Update locales + if ( ! isset( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + + if ( ! isset( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + /** * Prepare a single tax output for response. * From 141ae48a7991ae6d23afdd34ad00f11fed78e02b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 04:38:21 -0300 Subject: [PATCH 056/177] Added method to delete taxes --- includes/api/wc-rest-taxes-controller.php | 65 +++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 28f71b74584..4c59d5cf07a 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -80,7 +80,6 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { 'default' => false, 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ), - 'reassign' => array(), ), ), 'schema' => array( $this, 'get_public_item_schema' ), @@ -130,7 +129,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { } /** - * Check if a given request has access update a customer. + * Check if a given request has access update a tax. * * @param WP_REST_Request $request Full details about the request. * @return boolean @@ -143,6 +142,20 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return true; } + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get all taxes. * @@ -362,10 +375,10 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { $data[ $field ] = $request[ $key ]; } - // Update tax rate + // Update tax rate. WC_Tax::_update_tax_rate( $id, $data ); - // Update locales + // Update locales. if ( ! isset( $request['postcode'] ) ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); } @@ -394,6 +407,50 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { return $response; } + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + /** * Prepare a single tax output for response. * From 1509a2c8d80e975e27c216a3c3ccc7aac0ac3ed5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 04:47:52 -0300 Subject: [PATCH 057/177] Tax class schema --- .../api/wc-rest-tax-classes-controller.php | 82 ++++++++++++++++++- includes/api/wc-rest-taxes-controller.php | 2 +- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 254496c4655..1ac5bb00ce0 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -37,9 +37,89 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { protected $rest_base = 'taxes/classes'; /** - * Register the routes for coupons. + * Register the routes for tax classes. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get the User's schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax_class', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tax class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array(); } } diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 4c59d5cf07a..dfb58acc748 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -37,7 +37,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { protected $rest_base = 'taxes'; /** - * Register the routes for coupons. + * Register the routes for taxes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( From 03733d8757bab9ecc2c0693054d626fe4d9a172e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 04:56:35 -0300 Subject: [PATCH 058/177] Better WP_Error ids --- includes/api/wc-rest-customers-controller.php | 20 +++--- .../wc-rest-product-attributes-controller.php | 4 +- .../api/wc-rest-tax-classes-controller.php | 70 +++++++++++++++++++ includes/api/wc-rest-taxes-controller.php | 14 ++-- 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 700eb1604de..02ae7a2b69e 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -128,7 +128,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'create_users' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create_customer', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -146,7 +146,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $types = get_post_types( array( 'public' => true ), 'names' ); if ( empty( $id ) || empty( $customer->ID ) ) { - return new WP_Error( 'woocommerce_rest_customer_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } if ( get_current_user_id() === $id ) { @@ -154,9 +154,9 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } else if ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_rest_customer_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -188,7 +188,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $id = (int) $request['id']; if ( ! current_user_can( 'delete_user', $id ) ) { - return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -347,7 +347,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $customer = get_userdata( $id ); if ( empty( $id ) || empty( $customer->ID ) ) { - return new WP_Error( 'woocommerce_rest_customer_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $customer = $this->prepare_item_for_response( $customer, $request ); @@ -367,7 +367,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $customer = get_userdata( $id ); if ( ! $customer ) { - return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->user_email ) { @@ -375,7 +375,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } if ( ! empty( $request['username'] ) && $request['username'] !== $customer->user_login ) { - return new WP_Error( 'woocommerce_rest_user_invalid_argument', __( "Username isn't editable", 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable", 'woocommerce' ), array( 'status' => 400 ) ); } // Customer email. @@ -426,12 +426,12 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $customer = get_userdata( $id ); if ( ! $customer ) { - return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { - return new WP_Error( 'woocommerce_rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); } } diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 35578c52bdf..4f9f3869df6 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -231,7 +231,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { // Checks for errors. if ( is_wp_error( $insert ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create_product_attribute', $insert->get_error_message(), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', $insert->get_error_message(), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $wpdb->insert_id ); @@ -333,7 +333,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { // Checks for errors. if ( false === $update ) { - return new WP_Error( 'woocommerce_rest_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Could not edit the attribute', 'woocommerce' ), array( 'status' => 400 ) ); } $attribute = $this->get_attribute( $id ); diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 1ac5bb00ce0..85e94a59ea9 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -86,6 +86,76 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { ) ); } + /** + * Check whether a given request has permission to read tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list tax classes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get the User's schema, conforming to JSON Schema * diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index dfb58acc748..f857879b84c 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -108,7 +108,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create_tax', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -122,7 +122,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { */ public function get_item_permissions_check( $request ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_tax_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -150,7 +150,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { */ public function delete_item_permissions_check( $request ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -317,7 +317,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax_obj ) ) { - return new WP_Error( 'woocommerce_rest_tax_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $tax = $this->prepare_item_for_response( $tax_obj, $request ); @@ -337,7 +337,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { $current_tax = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $current_tax ) ) { - return new WP_Error( 'woocommerce_rest_tax_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } $data = array(); @@ -427,7 +427,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); if ( empty( $id ) || empty( $tax ) ) { - return new WP_Error( 'woocommerce_rest_user_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } $request->set_param( 'context', 'edit' ); @@ -513,7 +513,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * Prepare links for the request. * * @param stdClass $tax Tax object. - * @return array Links for the given user. + * @return array Links for the given tax. */ protected function prepare_links( $tax ) { $links = array( From 70dcae2994074c19a112e89ad04cb7d49cea6d4f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 05:16:37 -0300 Subject: [PATCH 059/177] List tax classes --- .../api/wc-rest-tax-classes-controller.php | 100 ++++++++++++++---- 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 85e94a59ea9..28befd12db5 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -57,14 +57,6 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), @@ -114,20 +106,6 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { return true; } - /** - * Check if a given request has access to read a tax class. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - /** * Check if a given request has access update a tax class. * @@ -156,6 +134,84 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { return true; } + /** + * Get all tax classes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $tax_classes = array(); + + // Add standard class. + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard Rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ); + } + + $data = array(); + foreach ( $tax_classes as $tax_class ) { + $class = $this->prepare_item_for_response( $tax_class, $request ); + $class = $this->prepare_response_for_collection( $class ); + $data[] = $class; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single tax class output for response. + * + * @param array $tax_class Tax class data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax_class, $request ) { + $data = $tax_class; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links() ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax_class Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); + } + + /** + * Prepare links for the request. + * + * @return array Links for the given tax class. + */ + protected function prepare_links() { + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + /** * Get the User's schema, conforming to JSON Schema * From 3eb9abf24f3a95fc8671c0f94eee404602f9b97e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 05:34:57 -0300 Subject: [PATCH 060/177] Create tax classes --- .../api/wc-rest-tax-classes-controller.php | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 28befd12db5..545b5191acd 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -56,7 +56,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P\w[\w\s\-]*)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), @@ -168,6 +168,58 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { return rest_ensure_response( $data ); } + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $exists = false; + $classes = WC_Tax::get_tax_classes(); + $tax_class = array( + 'slug' => sanitize_title( $request['name'] ), + 'name' => $request['name'], + ); + + // Check if class exists. + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $exists = true; + break; + } + } + + // Return error if tax class already exists. + if ( $exists ) { + return new WP_Error( 'woocommerce_rest_tax_class_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Add the new class. + $classes[] = $tax_class['name']; + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + $this->update_additional_fields_for_object( $tax_class, $request ); + + /** + * Fires after a tax class is created or updated via the REST API. + * + * @param stdClass $tax_class Data used to create the tax class. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax class, false when updating tax class. + */ + do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); + + return $response; + } + /** * Prepare a single tax class output for response. * @@ -233,6 +285,10 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { 'description' => __( 'Tax class name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), + 'required' => true, + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), ), ); From e493b80c5a4cd69d184bb411d2db89dd9e5b1e98 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 21:07:38 -0300 Subject: [PATCH 061/177] Added method to delete tax classes --- .../api/wc-rest-tax-classes-controller.php | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 545b5191acd..3ff0493c9fb 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -57,12 +57,6 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P\w[\w\s\-]*)', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -106,20 +100,6 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { return true; } - /** - * Check if a given request has access update a tax class. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - /** * Check if a given request has access delete a tax. * @@ -220,6 +200,73 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { return $response; } + /** + * Delete a single tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax_class = array( + 'slug' => sanitize_title( $request['slug'] ), + 'name' => '', + ); + $classes = WC_Tax::get_tax_classes(); + $deleted = false; + + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $tax_class['name'] = $class; + unset( $classes[ $key ] ); + $deleted = true; + break; + } + } + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + // Delete tax rate locations locations from the selected class. + $wpdb->query( $wpdb->prepare( " + DELETE locations.* + FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations + INNER JOIN + {$wpdb->prefix}woocommerce_tax_rates AS rates + ON rates.tax_rate_id = locations.tax_rate_id + WHERE rates.tax_rate_class = '%s' + ", $tax_class['slug'] ) ); + + // Delete tax rates in the selected class. + $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $tax_class['slug'] ), array( '%s' ) ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + + /** + * Fires after a tax class is deleted via the REST API. + * + * @param stdClass $tax_class The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); + + return $response; + } + /** * Prepare a single tax class output for response. * From 3e6ef07eadead8759236a07a002244c833ed33ae Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 21:34:14 -0300 Subject: [PATCH 062/177] Fixed get_item_schema() description --- includes/api/wc-rest-coupons-controller.php | 2 +- includes/api/wc-rest-customers-controller.php | 6 +++--- includes/api/wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/wc-rest-product-attributes-controller.php | 2 +- includes/api/wc-rest-product-categories-controller.php | 2 +- .../api/wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/wc-rest-product-tags-controller.php | 2 +- includes/api/wc-rest-tax-classes-controller.php | 2 +- includes/api/wc-rest-taxes-controller.php | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index e4d9c20cc16..739b9f9cdaf 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -382,7 +382,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { } /** - * Get the Post's schema, conforming to JSON Schema. + * Get the Coupon's schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 02ae7a2b69e..67e0d276dd7 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -591,8 +591,8 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { /** * Prepare links for the request. * - * @param WP_User $customer User object. - * @return array Links for the given user. + * @param WP_User $customer Customer object. + * @return array Links for the given customer. */ protected function prepare_links( $customer ) { $links = array( @@ -608,7 +608,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } /** - * Get the User's schema, conforming to JSON Schema + * Get the Customer's schema, conforming to JSON Schema * * @return array */ diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/wc-rest-product-attribute-terms-controller.php index 2f603893fed..3946bf3b1ea 100644 --- a/includes/api/wc-rest-product-attribute-terms-controller.php +++ b/includes/api/wc-rest-product-attribute-terms-controller.php @@ -72,7 +72,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle } /** - * Get the Term's schema, conforming to JSON Schema. + * Get the Attribute Term's schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/wc-rest-product-attributes-controller.php index 4f9f3869df6..679a984c48e 100644 --- a/includes/api/wc-rest-product-attributes-controller.php +++ b/includes/api/wc-rest-product-attributes-controller.php @@ -486,7 +486,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { } /** - * Get the Term's schema, conforming to JSON Schema. + * Get the Attribute's schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/wc-rest-product-categories-controller.php index ccb78ccfa06..b1a8073d35e 100644 --- a/includes/api/wc-rest-product-categories-controller.php +++ b/includes/api/wc-rest-product-categories-controller.php @@ -122,7 +122,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { } /** - * Get the Term's schema, conforming to JSON Schema. + * Get the Category schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/wc-rest-product-shipping-classes-controller.php index a35430a8194..3d0070e5c2a 100644 --- a/includes/api/wc-rest-product-shipping-classes-controller.php +++ b/includes/api/wc-rest-product-shipping-classes-controller.php @@ -106,7 +106,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll } /** - * Get the Term's schema, conforming to JSON Schema. + * Get the Shipping Class schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/wc-rest-product-tags-controller.php index 9096e0be867..230653d879f 100644 --- a/includes/api/wc-rest-product-tags-controller.php +++ b/includes/api/wc-rest-product-tags-controller.php @@ -80,7 +80,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { } /** - * Get the Term's schema, conforming to JSON Schema. + * Get the Tag's schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 3ff0493c9fb..633d76d5321 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -312,7 +312,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { } /** - * Get the User's schema, conforming to JSON Schema + * Get the Tax Classes schema, conforming to JSON Schema * * @return array */ diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index f857879b84c..144114df82e 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -529,7 +529,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { } /** - * Get the User's schema, conforming to JSON Schema + * Get the Taxes schema, conforming to JSON Schema * * @return array */ From 8e17808a5b98f4226a9377ff2b74ffaa4832f1a5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 21:46:31 -0300 Subject: [PATCH 063/177] Created reports endpoint --- includes/api/wc-rest-reports-controller.php | 129 ++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index b46d8c642f5..62c630b0ddf 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -40,6 +40,135 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { * Register the routes for coupons. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + /** + * Check whether a given request has permission to read reports. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list reports.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all public post types + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + + foreach ( $reports as $report ) { + $item = $this->prepare_item_for_response( (object) $report, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report 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( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'description' => $report->description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ) ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); } } From a1a56d5dbd7847da6744b9b9ff067ab36c0be5d8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 23:19:36 -0300 Subject: [PATCH 064/177] Created reports/sales endpoint --- .../api/wc-rest-report-sales-controller.php | 396 ++++++++++++++++++ includes/api/wc-rest-reports-controller.php | 30 +- includes/class-wc-api.php | 6 +- includes/wc-api-functions.php | 32 ++ 4 files changed, 447 insertions(+), 17 deletions(-) create mode 100644 includes/api/wc-rest-report-sales-controller.php diff --git a/includes/api/wc-rest-report-sales-controller.php b/includes/api/wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..a8904ceae87 --- /dev/null +++ b/includes/api/wc-rest-report-sales-controller.php @@ -0,0 +1,396 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read sales report. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list sales report.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get sales reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $item = $this->prepare_item_for_response( null, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param null $_ + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $_, $request ) { + // Set date filtering. + $filter = array( + 'period' => $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + // New customers. + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + $report_data = $this->report->get_report_data(); + $period_totals = array(); + + // Setup period totals by ensuring each period in the interval has data. + for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + default : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // Set the customer signups for each period. + $customer_count = 0; + foreach ( $customers as $customer ) { + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $report_data->orders as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + foreach ( $report_data->order_counts as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['orders'] = (int) $order->count; + } + + // Add total order items for each period. + foreach ( $report_data->order_items as $order_item ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // Add total discount for each period. + foreach ( $report_data->coupons as $discount ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $data = array( + 'total_sales' => $report_data->total_sales, + 'net_sales' => $report_data->net_sales, + 'average_sales' => $report_data->average_sales, + 'total_orders' => $report_data->total_orders, + 'total_items' => $report_data->total_items, + 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), + 'total_shipping' => $report_data->total_shipping, + 'total_refunds' => $report_data->total_refunds, + 'total_discount' => $report_data->total_coupons, + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + ) ); + + /** + * Filter a report sales returned from the API. + * + * Allows modification of the report sales data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $data The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $data, $request ); + } + + /** + * Setup the report object and parse any date filtering. + * + * @param array $filter date filtering + */ + protected function setup_report( $filter ) { + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); + + $this->report = new WC_Report_Sales_By_Date(); + + if ( empty( $filter['period'] ) ) { + + // Custom date range. + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // Iverwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. + $_GET['start_date'] = $filter['date_min']; + $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; + + } else { + + // Default custom range to today. + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + + } else { + + // Change "week" period to "7day". + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'sales_report', + 'type' => 'object', + 'properties' => array( + 'total_sales' => array( + 'description' => __( 'Gross sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'net_sales' => array( + 'description' => __( 'Net sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'average_sales' => array( + 'description' => __( 'Average net daily sales.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_orders' => array( + 'description' => __( 'Total of orders placed.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_items' => array( + 'description' => __( 'Total of items purchased.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Total charged for taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_shipping' => array( + 'description' => __( 'Total charged for shipping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_refunds' => array( + 'description' => __( 'Total of refunded orders.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_discount' => array( + 'description' => __( 'Total of coupons used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals_grouped_by' => array( + 'description' => __( 'Group type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals' => array( + 'description' => __( 'Totals.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'period' => array( + 'description' => __( 'Report period.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'week', 'month', 'last_month', 'year' ), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_min' => array( + 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_max' => array( + 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/wc-rest-reports-controller.php index 62c630b0ddf..79441b54635 100644 --- a/includes/api/wc-rest-reports-controller.php +++ b/includes/api/wc-rest-reports-controller.php @@ -66,7 +66,7 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { } /** - * Get all public post types + * Get all reports. * * @param WP_REST_Request $request * @return array|WP_Error @@ -139,21 +139,21 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { */ public function get_item_schema() { $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'report', - 'type' => 'object', - 'properties' => array( - 'slug' => array( - 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view' ), - 'readonly' => true, + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, ), - 'description' => array( - 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view' ), - 'readonly' => true, + 'description' => array( + 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, ), ), ); diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 8178aceb227..9720e00c8eb 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -379,12 +379,13 @@ class WC_API { include_once( 'api/wc-rest-order-notes-controller.php' ); include_once( 'api/wc-rest-order-refunds-controller.php' ); include_once( 'api/wc-rest-orders-controller.php' ); - include_once( 'api/wc-rest-product-attributes-controller.php' ); include_once( 'api/wc-rest-product-attribute-terms-controller.php' ); + include_once( 'api/wc-rest-product-attributes-controller.php' ); include_once( 'api/wc-rest-product-categories-controller.php' ); - include_once( 'api/wc-rest-products-controller.php' ); include_once( 'api/wc-rest-product-shipping-classes-controller.php' ); include_once( 'api/wc-rest-product-tags-controller.php' ); + include_once( 'api/wc-rest-products-controller.php' ); + include_once( 'api/wc-rest-report-sales-controller.php' ); include_once( 'api/wc-rest-reports-controller.php' ); include_once( 'api/wc-rest-tax-classes-controller.php' ); include_once( 'api/wc-rest-taxes-controller.php' ); @@ -408,6 +409,7 @@ class WC_API { 'WC_REST_Product_Shipping_Classes_Controller', 'WC_REST_Product_Tags_Controller', 'WC_REST_Products_Controller', + 'WC_REST_Report_Sales_Controller', 'WC_REST_Reports_Controller', 'WC_REST_Tax_Classes_Controller', 'WC_REST_Taxes_Controller', diff --git a/includes/wc-api-functions.php b/includes/wc-api-functions.php index afa177750a2..91fb1241087 100644 --- a/includes/wc-api-functions.php +++ b/includes/wc-api-functions.php @@ -150,3 +150,35 @@ function wc_rest_api_set_uploaded_image_as_attachment( $upload, $id = 0 ) { return $attachment_id; } + +/** + * Validate reports request arguments. + * + * @since 2.6.0 + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return WP_Error|boolean + */ +function rest_validate_reports_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return true; + } + $args = $attributes['args'][ $param ]; + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'string' ) ); + } + + if ( 'data' === $args['format'] ) { + $regex = '#^\d{4}-\d{2}-\d{2}$#'; + + if ( ! preg_match( $regex, $value, $matches ) ) { + return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); + } + } + + return true; +} From 4d633440ef069f830d647d0d7a0647600a2d4557 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 9 Mar 2016 23:51:25 -0300 Subject: [PATCH 065/177] Top sellers endpoint --- .../api/wc-rest-report-sales-controller.php | 19 +- .../wc-rest-report-top-sellers-controller.php | 174 ++++++++++++++++++ includes/class-wc-api.php | 2 + 3 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 includes/api/wc-rest-report-top-sellers-controller.php diff --git a/includes/api/wc-rest-report-sales-controller.php b/includes/api/wc-rest-report-sales-controller.php index a8904ceae87..8e20274f34f 100644 --- a/includes/api/wc-rest-report-sales-controller.php +++ b/includes/api/wc-rest-report-sales-controller.php @@ -2,7 +2,7 @@ /** * REST API Reports controller * - * Handles requests to the reports endpoint. + * Handles requests to the reports/sales endpoint. * * @author WooThemes * @category API @@ -59,14 +59,14 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { } /** - * Check whether a given request has permission to read sales report. + * Check whether a given request has permission to read report. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list sales report.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list reports.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -198,7 +198,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } - $data = array( + $sales_data = array( 'total_sales' => $report_data->total_sales, 'net_sales' => $report_data->net_sales, 'average_sales' => $report_data->average_sales, @@ -214,16 +214,13 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->add_additional_fields_to_object( $sales_data, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - 'collection' => array( + 'about' => array( 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), ), ) ); @@ -237,7 +234,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { * @param stdClass $data The original report object. * @param WP_REST_Request $request Request used to generate the response. */ - return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $data, $request ); + return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request ); } /** @@ -270,6 +267,8 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { } else { + $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period']; + // Change "week" period to "7day". if ( 'week' === $filter['period'] ) { $filter['period'] = '7day'; diff --git a/includes/api/wc-rest-report-top-sellers-controller.php b/includes/api/wc-rest-report-top-sellers-controller.php new file mode 100644 index 00000000000..6c7bb6b791a --- /dev/null +++ b/includes/api/wc-rest-report-top-sellers-controller.php @@ -0,0 +1,174 @@ + $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + $report_data = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ) + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers = array(); + + foreach ( $report_data as $item ) { + $product = wc_get_product( $item->product_id ); + + if ( $product ) { + $top_sellers[] = array( + 'title' => $product->get_title(), + 'product_id' => (int) $item->product_id, + 'quantity' => wc_stock_amount( $item->order_item_qty ), + ); + } + } + + $data = array(); + foreach ( $top_sellers as $top_seller ) { + $item = $this->prepare_item_for_response( (object) $top_seller, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param stdClass $top_seller + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $top_seller, $request ) { + $data = array( + 'title' => $top_seller->title, + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->quantity, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'about' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ), + ), + ) ); + + /** + * Filter a report top sellers returned from the API. + * + * Allows modification of the report top sellers data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $top_seller The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'top_sellers_report', + 'type' => 'object', + 'properties' => array( + 'title' => array( + 'description' => __( 'Product title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Total of purchases.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 9720e00c8eb..f597a0e2f62 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -386,6 +386,7 @@ class WC_API { include_once( 'api/wc-rest-product-tags-controller.php' ); include_once( 'api/wc-rest-products-controller.php' ); include_once( 'api/wc-rest-report-sales-controller.php' ); + include_once( 'api/wc-rest-report-top-sellers-controller.php' ); include_once( 'api/wc-rest-reports-controller.php' ); include_once( 'api/wc-rest-tax-classes-controller.php' ); include_once( 'api/wc-rest-taxes-controller.php' ); @@ -410,6 +411,7 @@ class WC_API { 'WC_REST_Product_Tags_Controller', 'WC_REST_Products_Controller', 'WC_REST_Report_Sales_Controller', + 'WC_REST_Report_Top_Sellers_Controller', 'WC_REST_Reports_Controller', 'WC_REST_Tax_Classes_Controller', 'WC_REST_Taxes_Controller', From ea9fed64ad2544bfc8e96da56ae7d777264a398a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Sat, 12 Mar 2016 05:15:30 -0300 Subject: [PATCH 066/177] Webhooks --- includes/api/wc-rest-webhooks-controller.php | 45 ++++++++++++++++++++ includes/class-wc-api.php | 1 + 2 files changed, 46 insertions(+) create mode 100644 includes/api/wc-rest-webhooks-controller.php diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..e72118cbebb --- /dev/null +++ b/includes/api/wc-rest-webhooks-controller.php @@ -0,0 +1,45 @@ + Date: Tue, 15 Mar 2016 16:37:10 -0300 Subject: [PATCH 067/177] Webhook schema --- includes/api/wc-rest-taxes-controller.php | 2 +- includes/api/wc-rest-webhooks-controller.php | 130 +++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/wc-rest-taxes-controller.php index 144114df82e..9e9f8bc8335 100644 --- a/includes/api/wc-rest-taxes-controller.php +++ b/includes/api/wc-rest-taxes-controller.php @@ -529,7 +529,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { } /** - * Get the Taxes schema, conforming to JSON Schema + * Get the Taxes schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index e72118cbebb..1ee3b54d217 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -40,6 +40,136 @@ class WC_REST_Webhooks_Controller extends WP_REST_Controller { * Register the routes for webhooks. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array( 'active', 'paused', 'disabled' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wc_is_webhook_valid_topic', + ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to the current API user's consumer secret if not provided.", 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'edit' ), + 'writeonly' => true, + ), + 'created_at' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated_at' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From 2808658e1fbed6bfe9e1d369d4cc2b3bf4c3b213 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:38:50 -0300 Subject: [PATCH 068/177] Webhook permissions --- includes/api/wc-rest-webhooks-controller.php | 72 +++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index 1ee3b54d217..391f63eba76 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -86,6 +86,76 @@ class WC_REST_Webhooks_Controller extends WP_REST_Controller { ) ); } + /** + * Check whether a given request has permission to read webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list webhooks.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Get the Webhook's schema, conforming to JSON Schema. * @@ -94,7 +164,7 @@ class WC_REST_Webhooks_Controller extends WP_REST_Controller { public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'tax', + 'title' => 'webhook', 'type' => 'object', 'properties' => array( 'id' => array( From d67000fb6cc36850827bfeef65f1d62703328fb6 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:45:29 -0300 Subject: [PATCH 069/177] Better prepare coupons hook name --- includes/api/wc-rest-coupons-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 739b9f9cdaf..687eb4a109a 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -154,7 +154,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ - return apply_filters( 'woocommerce_rest_prepare_' . $this->post_type, $response, $post, $request ); + return apply_filters( 'woocommerce_rest_prepare_coupon', $response, $post, $request ); } /** From 218390071c03bdc2e3e6cb00185318fb0a1eeeaf Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:45:46 -0300 Subject: [PATCH 070/177] Improve posts controller --- .../abstract-wc-rest-posts-controller.php | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index eb021065bd0..4b47596462c 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -340,17 +340,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return rest_ensure_response( $response ); } - /** - * Update post meta fields. - * - * @param WP_Post $post - * @param WP_REST_Request $request - * @return bool|WP_Error - */ - protected function update_post_meta_fields( $post, $request ) { - return true; - } - /** * Get a collection of posts. * @@ -457,6 +446,76 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0; + + /** + * Filter whether an item is trashable. + * + * Return false to disable trash support for the item. + * + * @param boolean $supports_trash Whether the item type support trashing. + * @param WP_Post $post The Post object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); + + if ( ! $this->check_delete_permission( $post ) ) { + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $result = wp_delete_post( $id, true ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' === $post->post_status ) { + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $result = wp_trash_post( $id ); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + return $response; + } + /** * Determine the allowed query_vars for a get_items() response and * prepare for WP_Query. @@ -650,72 +709,13 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } /** - * Delete a single item. + * Update post meta fields. * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error */ - public function delete_item( $request ) { - $id = (int) $request['id']; - $force = (bool) $request['force']; - - $post = get_post( $id ); - - if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $supports_trash = EMPTY_TRASH_DAYS > 0; - - /** - * Filter whether an item is trashable. - * - * Return false to disable trash support for the item. - * - * @param boolean $supports_trash Whether the item type support trashing. - * @param WP_Post $post The Post object being considered for trashing support. - */ - $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); - - if ( ! $this->check_delete_permission( $post ) ) { - return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); - } - - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - - // If we're forcing, then delete permanently. - if ( $force ) { - $result = wp_delete_post( $id, true ); - } else { - // If we don't support trashing for this type, error out. - if ( ! $supports_trash ) { - return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); - } - - // Otherwise, only trash if we haven't already. - if ( 'trash' === $post->post_status ) { - return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); - } - - // (Note that internally this falls through to `wp_delete_post` if - // the trash is disabled.) - $result = wp_trash_post( $id ); - } - - if ( ! $result ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); - } - - /** - * Fires after a single item is deleted or trashed via the REST API. - * - * @param object $post The deleted or trashed item. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); - - return $response; + protected function update_post_meta_fields( $post, $request ) { + return true; } } From 3b22acb2b17ffb3fbcab0806014287babbe6d8b5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:53:51 -0300 Subject: [PATCH 071/177] Add links to coupons responses --- includes/api/wc-rest-coupons-controller.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 687eb4a109a..cd9a44b764a 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -144,6 +144,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { // Wrap the data in a response object. $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $post ) ); + /** * Filter the data for a response. * @@ -157,6 +159,25 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { return apply_filters( 'woocommerce_rest_prepare_coupon', $response, $post, $request ); } + /** + * Prepare links for the request. + * + * @param WP_Post $post Customer object. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + /** * Prepare a single coupon for create or update. * From f53e582566efa51571a02f9cecba0853149932bc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:55:41 -0300 Subject: [PATCH 072/177] Include prepare_links() to abstract posts controller --- .../abstract-wc-rest-posts-controller.php | 20 +++++++++++++++++++ includes/api/wc-rest-coupons-controller.php | 19 ------------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 4b47596462c..4601d7af77e 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -516,6 +516,26 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } + /** + * Prepare links for the request. + * + * @param WP_Post $post Post object. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** * Determine the allowed query_vars for a get_items() response and * prepare for WP_Query. diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index cd9a44b764a..e3c61111796 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -159,25 +159,6 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { return apply_filters( 'woocommerce_rest_prepare_coupon', $response, $post, $request ); } - /** - * Prepare links for the request. - * - * @param WP_Post $post Customer object. - * @return array Links for the given post. - */ - protected function prepare_links( $post ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), - ); - - return $links; - } - /** * Prepare a single coupon for create or update. * From 8dc92eb284cc6d8ce4a572928d4ca3afbabc2fd1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 16:56:55 -0300 Subject: [PATCH 073/177] Added methods to get and list webhooks --- includes/api/wc-rest-webhooks-controller.php | 54 +++++++++++++++++++- includes/class-wc-api.php | 1 + 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index 391f63eba76..493f3180023 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Webhooks controller class. * * @package WooCommerce/API - * @extends WP_REST_Controller + * @extends WC_REST_Posts_Controller */ -class WC_REST_Webhooks_Controller extends WP_REST_Controller { +class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { /** * Endpoint namespace. @@ -36,6 +36,13 @@ class WC_REST_Webhooks_Controller extends WP_REST_Controller { */ protected $rest_base = 'webhooks'; + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_webhook'; + /** * Register the routes for webhooks. */ @@ -156,6 +163,49 @@ class WC_REST_Webhooks_Controller extends WP_REST_Controller { return true; } + /** + * Prepare a single webhook output for response. + * + * @param WP_Post $webhook Webhook object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $post, $request ) { + $id = (int) $post->ID; + $webhook = new WC_Webhook( $id ); + + $data = array( + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'created_at' => wc_rest_api_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'updated_at' => wc_rest_api_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook', $response, $webhook, $request ); + } + /** * Get the Webhook's schema, conforming to JSON Schema. * diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index e98446c513b..03524ccb772 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -416,6 +416,7 @@ class WC_API { 'WC_REST_Reports_Controller', 'WC_REST_Tax_Classes_Controller', 'WC_REST_Taxes_Controller', + 'WC_REST_Webhooks_Controller', ); foreach ( $controllers as $controller ) { From 696aa2b7fecadbff104467a2d0998beb8883f186 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 17:22:53 -0300 Subject: [PATCH 074/177] Added endpoint for create webhooks --- includes/api/wc-rest-coupons-controller.php | 2 +- includes/api/wc-rest-webhooks-controller.php | 145 ++++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index e3c61111796..9d0fe37dab2 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -156,7 +156,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. */ - return apply_filters( 'woocommerce_rest_prepare_coupon', $response, $post, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); } /** diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index 493f3180023..cd5e4f7e3b1 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -58,7 +58,17 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'topic' => array( + 'required' => true, + ), + 'delivery_url' => array( + 'required' => true, + ), + 'secret' => array( + 'required' => true, + ), + ) ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -163,6 +173,137 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return true; } + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + // Validate topic. + if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + $webhook = new WC_Webhook( $post->ID ); + + // Set topic. + $webhook->set_topic( $request['topic'] ); + + // Set delivery URL. + $webhook->set_delivery_url( $request['delivery_url'] ); + + // Set secret. + $webhook->set_secret( $request['secret'] ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + + /** + * Prepare a single webhook for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $data = new stdClass; + + // Post ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $password = strlen( uniqid( 'webhook_' ) ); + $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $password; + + // Post status. + $data->post_status = 'publish'; + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + } else { + + // Allow edit post title. + if ( ! empty( $request['name'] ) ) { + $data->post_title = $request['name']; + } + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + /** * Prepare a single webhook output for response. * @@ -203,7 +344,7 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { * @param WC_Webhook $webhook Webhook object used to create response. * @param WP_REST_Request $request Request object. */ - return apply_filters( 'woocommerce_rest_prepare_webhook', $response, $webhook, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); } /** From 8cb8dfc3a87b96714eb4b578f462d410c8d649e4 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 17:39:23 -0300 Subject: [PATCH 075/177] Added method to update webhooks --- includes/api/wc-rest-webhooks-controller.php | 113 +++++++++++++++++-- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index cd5e4f7e3b1..687a572ae27 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -174,7 +174,7 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { } /** - * Create a single item. + * Create a single webhook. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response @@ -189,6 +189,11 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); } + // Validate delivery URL. + if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; @@ -208,10 +213,7 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { } $post->ID = $post_id; - $post = get_post( $post_id ); - $this->update_additional_fields_for_object( $post, $request ); - - $webhook = new WC_Webhook( $post->ID ); + $webhook = new WC_Webhook( $post_id ); // Set topic. $webhook->set_topic( $request['topic'] ); @@ -222,6 +224,14 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { // Set secret. $webhook->set_secret( $request['secret'] ); + // Set status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + /** * Fires after a single item is created or updated via the REST API. * @@ -246,6 +256,87 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return $response; } + /** + * Update a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $webhook = new WC_Webhook( $id ); + + // Update topic. + if ( ! empty( $request['topic'] ) ) { + if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + $webhook->set_topic( $request['topic'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update delivery URL. + if ( ! empty( $request['delivery_url'] ) ) { + if ( wc_is_valid_url( $request['delivery_url'] ) ) { + $webhook->set_delivery_url( $request['delivery_url'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update secret. + if ( ! empty( $request['secret'] ) ) { + $webhook->set_secret( $request['secret'] ); + } + + // Update status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return rest_ensure_response( $response ); + } + /** * Prepare a single webhook for create or update. * @@ -277,12 +368,6 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { // Post status. $data->post_status = 'publish'; - - // Comment status. - $data->comment_status = 'closed'; - - // Ping status. - $data->ping_status = 'closed'; } else { // Allow edit post title. @@ -291,6 +376,12 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { } } + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + /** * Filter the query_vars used in `get_items` for the constructed query. * From 3775aef6dae4b20a055cfd92ef3dffbdfbfcd660 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 17:46:52 -0300 Subject: [PATCH 076/177] Added method to delete webhooks --- includes/api/wc-rest-webhooks-controller.php | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index 687a572ae27..c794309e888 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -337,6 +337,51 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return rest_ensure_response( $response ); } + /** + * Delete a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + $result = wp_delete_post( $id, true ); + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + return $response; + } + /** * Prepare a single webhook for create or update. * From f3f16944ca7c587d42f7bfaba17e248ffba243b7 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 17:54:06 -0300 Subject: [PATCH 077/177] Created webhook deliveries class --- includes/api/wc-rest-webhook-deliveries.php | 65 +++++++++++++++++++++ includes/class-wc-api.php | 2 + 2 files changed, 67 insertions(+) create mode 100644 includes/api/wc-rest-webhook-deliveries.php diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/wc-rest-webhook-deliveries.php new file mode 100644 index 00000000000..4a4b465817d --- /dev/null +++ b/includes/api/wc-rest-webhook-deliveries.php @@ -0,0 +1,65 @@ +[\d]+)/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + public $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'webhooks/(?P[\d]+)/deliveries'; + + /** + * Register the routes for webhook deliveries. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 03524ccb772..8604f27e9ed 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -390,6 +390,7 @@ class WC_API { include_once( 'api/wc-rest-reports-controller.php' ); include_once( 'api/wc-rest-tax-classes-controller.php' ); include_once( 'api/wc-rest-taxes-controller.php' ); + include_once( 'api/wc-rest-webhook-deliveries.php' ); include_once( 'api/wc-rest-webhooks-controller.php' ); } @@ -416,6 +417,7 @@ class WC_API { 'WC_REST_Reports_Controller', 'WC_REST_Tax_Classes_Controller', 'WC_REST_Taxes_Controller', + 'WC_REST_Webhook_Deliveries_Controller', 'WC_REST_Webhooks_Controller', ); From ebc397df317ad3f8ca9420280e4dd597b4f8aba2 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 18:19:34 -0300 Subject: [PATCH 078/177] Allow query webhooks by status --- includes/api/wc-rest-webhook-deliveries.php | 44 ++++++++++++++++ includes/api/wc-rest-webhooks-controller.php | 54 ++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/wc-rest-webhook-deliveries.php index 4a4b465817d..d41a58a74b4 100644 --- a/includes/api/wc-rest-webhook-deliveries.php +++ b/includes/api/wc-rest-webhook-deliveries.php @@ -62,4 +62,48 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + + + + 'created_at' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } } diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index c794309e888..917e4f1c6f5 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -43,6 +43,13 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'shop_webhook'; + /** + * Initialize Webhooks actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + /** * Register the routes for webhooks. */ @@ -483,6 +490,53 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); } + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + switch ( $request['status'] ) { + case 'active' : + $args['post_status'] = 'publish'; + break; + case 'paused' : + $args['post_status'] = 'draft'; + break; + case 'disabled' : + $args['post_status'] = 'pending'; + break; + default : + $args['post_status'] = 'any'; + break; + } + + return $args; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + /** * Get the Webhook's schema, conforming to JSON Schema. * From 56a4df6eb56abbdd99feb0635a35ee15c145d011 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 19:02:13 -0300 Subject: [PATCH 079/177] Webhook deliveries schema --- includes/api/wc-rest-webhook-deliveries.php | 59 ++++++++++++++++++-- includes/api/wc-rest-webhooks-controller.php | 6 +- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/wc-rest-webhook-deliveries.php index d41a58a74b4..09a3ca5201d 100644 --- a/includes/api/wc-rest-webhook-deliveries.php +++ b/includes/api/wc-rest-webhook-deliveries.php @@ -77,24 +77,71 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view' ), 'readonly' => true, ), 'duration' => array( 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view' ), 'readonly' => true, ), 'summary' => array( 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'Request headers.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_body' => array( + 'description' => __( 'Request body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_code' => array( + 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_message' => array( + 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_headers' => array( + 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), 'readonly' => true, ), - - - 'created_at' => array( 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index 917e4f1c6f5..b06e336cd3f 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -528,10 +528,10 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { $params['status'] = array( 'default' => 'all', 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), - 'sanitize_callback' => 'sanitize_key', 'type' => 'string', - 'enum' => array( 'all', 'active', 'paused', 'disabled' ), - 'validate_callback' => 'rest_validate_request_arg', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', ); return $params; From bd3b5e986e88d21e7e0c932637e5e95f57a70b5b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 19:38:38 -0300 Subject: [PATCH 080/177] Added default context for tax classes --- includes/api/wc-rest-tax-classes-controller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/wc-rest-tax-classes-controller.php index 633d76d5321..404d133cf3c 100644 --- a/includes/api/wc-rest-tax-classes-controller.php +++ b/includes/api/wc-rest-tax-classes-controller.php @@ -349,6 +349,8 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * @return array */ public function get_collection_params() { - return array(); + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); } } From 0ab93eebeb32aae520143a1b57510823d585756d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 19:38:48 -0300 Subject: [PATCH 081/177] Crated methods to list and get webhook deliveries --- includes/api/wc-rest-webhook-deliveries.php | 151 ++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/wc-rest-webhook-deliveries.php index 09a3ca5201d..3c9b00d594d 100644 --- a/includes/api/wc-rest-webhook-deliveries.php +++ b/includes/api/wc-rest-webhook-deliveries.php @@ -63,6 +63,146 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { ) ); } + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list taxes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook develivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all webhook deliveries. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $id = (int) $request['id']; + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = $webhook->get_delivery_logs(); + + $data = array(); + foreach ( $logs as $log ) { + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $delivery = $this->prepare_response_for_collection( $delivery ); + $data[] = $delivery; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single webhook delivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = $webhook->get_delivery_log( $id ); + + if ( empty( $id ) || empty( $log ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + // Add timestamp. + $data['created_at'] = wc_rest_api_prepare_date_response( $log->comment->comment_date_gmt ); + + // Remove comment object. + unset( $data['comment'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $log Delivery log object. + * @return array Links for the given webhook delivery. + */ + protected function prepare_links( $log ) { + $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; + $base = str_replace( '(?P[\d]+)', $webhook_id, $this->rest_base ); + + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/wc/v1/webhooks/%d', $webhook_id ) ), + ), + ); + + return $links; + } + /** * Get the Webhook's schema, conforming to JSON Schema. * @@ -153,4 +293,15 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } } From 708c872107d705d3f3806557cdbd6a66eabb44f7 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 15 Mar 2016 19:40:54 -0300 Subject: [PATCH 082/177] Fixed webhook deliveries schema --- includes/api/wc-rest-webhook-deliveries.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/wc-rest-webhook-deliveries.php index 3c9b00d594d..5767bd04c51 100644 --- a/includes/api/wc-rest-webhook-deliveries.php +++ b/includes/api/wc-rest-webhook-deliveries.php @@ -248,7 +248,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { ), 'request_headers' => array( 'description' => __( 'Request headers.', 'woocommerce' ), - 'type' => 'object', + 'type' => 'array', 'context' => array( 'view' ), 'readonly' => true, ), @@ -278,7 +278,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { ), 'response_body' => array( 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), - 'type' => 'array', + 'type' => 'string', 'context' => array( 'view' ), 'readonly' => true, ), From eb5db423ccbfd6c6a2923d47e71744b76aab7edc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 06:53:09 -0300 Subject: [PATCH 083/177] Handle basic authentication on REST API --- includes/api/wc-rest-authentication.php | 123 ++++++++++++++++++++++++ includes/class-wc-api.php | 3 + 2 files changed, 126 insertions(+) create mode 100644 includes/api/wc-rest-authentication.php diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php new file mode 100644 index 00000000000..4884ebe4b61 --- /dev/null +++ b/includes/api/wc-rest-authentication.php @@ -0,0 +1,123 @@ +perform_basic_authentication() ) { + $user_id = $new_user_id; + } + } + + return $user_id; + } + + /** + * Check for authentication error. + * + * @param WP_Error|null|bool $error + * @return WP_Error|null|bool + */ + public function check_authentication_error( $error ) { + global $wc_rest_authentication_error; + + // Passthrough other errors. + if ( ! empty( $error ) ) { + return $error; + } + + return $wc_rest_authentication_error; + } + + /** + * Basic Authentication. + * + * SSL-encrypted requests are not subject to sniffing or man-in-the-middle + * attacks, so the request can be authenticated by simply looking up the user + * associated with the given consumer key and confirming the consumer secret + * provided is valid. + * + * @return int + */ + private function perform_basic_authentication() { + global $wc_rest_authentication_error; + + $user = null; + $user_id = 0; + $consumer_key = ''; + $consumer_secret = ''; + + // If the $_GET parameters are present, use those first. + if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { + $consumer_key = $_GET['consumer_key']; + $consumer_secret = $_GET['consumer_secret']; + } + + // If the above is not present, we will do full basic auth. + if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) { + $consumer_key = $_SERVER['PHP_AUTH_USER']; + $consumer_secret = $_SERVER['PHP_AUTH_PW']; + } + + // Get user data. + if ( $consumer_key && $consumer_secret ) { + $user = $this->get_user_data_by_consumer_key( $consumer_key ); + $user_id = $user->user_id; + } + + // Validate user secret. + if ( empty( $user ) || ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid', 'woocommerce' ), array( 'status' => 401 ) ); + $user_id = 0; + } + + return $user_id; + } + + /** + * Return the user data for the given consumer_key. + * + * @param string $consumer_key + * @return array + */ + private function get_user_data_by_consumer_key( $consumer_key ) { + global $wpdb; + + $consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) ); + $user = $wpdb->get_row( $wpdb->prepare( " + SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE consumer_key = '%s' + ", $consumer_key ) ); + + return $user; + } +} + +new WC_REST_Authentication(); diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 8604f27e9ed..691b5313aab 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -363,6 +363,9 @@ class WC_API { * @since 2.6.0 */ private function rest_api_includes() { + // Authentication. + include_once( 'api/wc-rest-authentication.php' ); + // WP-API classes and functions. include_once( 'vendor/wp-api-functions.php' ); if ( ! class_exists( 'WP_REST_Controller' ) ) { From ed2d72fc0dcc0a2120892ca7f27af620c5dfdf0d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 07:24:59 -0300 Subject: [PATCH 084/177] Verify API Key permissions --- includes/api/wc-rest-authentication.php | 50 ++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index 4884ebe4b61..25aeb97f999 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -91,12 +91,21 @@ class WC_REST_Authentication { $user_id = $user->user_id; } + // Abort if don't have an user at this point. + if ( empty( $user ) ) { + return $user_id; + } + // Validate user secret. - if ( empty( $user ) || ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { + if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid', 'woocommerce' ), array( 'status' => 401 ) ); $user_id = 0; } + if ( ! $this->check_permissions( $user->permissions ) ) { + $user_id = 0; + } + return $user_id; } @@ -118,6 +127,45 @@ class WC_REST_Authentication { return $user; } + + /** + * Check that the API keys provided have the proper key-specific permissions to either read or write API resources. + * + * @param string $permissions + * @return bool + */ + private function check_permissions( $permissions ) { + global $wc_rest_authentication_error; + + $valid = true; + + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) { + return false; + } + + switch ( $_SERVER['REQUEST_METHOD'] ) { + + case 'HEAD': + case 'GET': + if ( 'read' !== $permissions && 'read_write' !== $permissions ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions', 'woocommerce' ), array( 'status' => 401 ) ); + $valid = false; + } + break; + + case 'POST': + case 'PUT': + case 'PATCH': + case 'DELETE': + if ( 'write' !== $permissions && 'read_write' !== $permissions ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions', 'woocommerce' ), array( 'status' => 401 ) ); + $valid = false; + } + break; + } + + return $valid; + } } new WC_REST_Authentication(); From a0b0489014b307eb7ef7e575ae7907b47fd1615f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 08:37:31 -0300 Subject: [PATCH 085/177] Handle oauth1.0a authentication on REST API --- includes/api/wc-rest-authentication.php | 255 ++++++++++++++++++++++-- includes/wc-api-functions.php | 17 ++ 2 files changed, 260 insertions(+), 12 deletions(-) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index 25aeb97f999..3d59f08b43b 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -18,7 +18,7 @@ class WC_REST_Authentication { * Initialize authentication actions. */ public function __construct() { - add_filter( 'determine_current_user', array( $this, 'authenticate' ), 30 ); + add_filter( 'determine_current_user', array( $this, 'authenticate' ), 100 ); add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ) ); } @@ -29,9 +29,18 @@ class WC_REST_Authentication { * @return int */ public function authenticate( $user_id ) { + // Do not authenticate twice! + if ( ! empty( $user_id ) ) { + return $user_id; + } + if ( is_ssl() ) { - if ( $new_user_id = $this->perform_basic_authentication() ) { - $user_id = $new_user_id; + if ( $user_id = $this->perform_basic_authentication() ) { + return $user_id; + } + } else { + if ( $user_id = $this->perform_oauth_authentication() ) { + return $user_id; } } @@ -93,22 +102,227 @@ class WC_REST_Authentication { // Abort if don't have an user at this point. if ( empty( $user ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + return $user_id; } // Validate user secret. if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { - $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid', 'woocommerce' ), array( 'status' => 401 ) ); + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); $user_id = 0; } + // Check API Key permissions. if ( ! $this->check_permissions( $user->permissions ) ) { $user_id = 0; } + // Update last access. + $this->update_last_access( $user->key_id ); + return $user_id; } + /** + * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests. + * + * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP. + * + * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions: + * + * 1) There is no token associated with request/responses, only consumer keys/secrets are used. + * + * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header, + * This is because there is no cross-OS function within PHP to get the raw Authorization header. + * + * @link http://tools.ietf.org/html/rfc5849 for the full spec. + * @since 2.1 + * @return array + * @throws Exception + */ + private function perform_oauth_authentication() { + global $wc_rest_authentication_error; + + $params = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' ); + + // Check for required OAuth parameters. + foreach ( $params as $param ) { + if ( empty( $_GET[ $param ] ) ) { + return 0; + } + } + + // Fetch WP user by consumer key + $user = $this->get_user_data_by_consumer_key( $_GET['oauth_consumer_key'] ); + + if ( empty( $user ) ) { + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); + + return $user_id; + } + + // Perform OAuth validation. + $wc_rest_authentication_error = $this->check_oauth_signature( $user, $_GET ); + if ( is_wp_error( $wc_rest_authentication_error ) ) { + return 0; + } + + $wc_rest_authentication_error = $this->check_oauth_timestamp_and_nonce( $user, $_GET['oauth_timestamp'], $_GET['oauth_nonce'] ); + if ( is_wp_error( $wc_rest_authentication_error ) ) { + return 0; + } + + // Check API Key permissions. + if ( ! $this->check_permissions( $user->permissions ) ) { + $user_id = 0; + } + + // Update last access. + $this->update_last_access( $user->key_id ); + + return $user->user_id; + } + + /** + * Verify that the consumer-provided request signature matches our generated signature, + * this ensures the consumer has a valid key/secret. + * + * @param stdClass $user + * @param array $params The request parameters. + * @return null|WP_Error + */ + private function check_oauth_signature( $user, $params ) { + $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); + + $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); + $wp_base = get_home_url( null, '/', 'relative' ); + if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { + $request_path = substr( $request_path, strlen( $wp_base ) ); + } + $base_request_uri = rawurlencode( get_home_url( null, $request_path ) ); + + // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. + $consumer_signature = rawurldecode( $params['oauth_signature'] ); + unset( $params['oauth_signature'] ); + + // Sort parameters. + if ( ! uksort( $params, 'strcmp' ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), array( 'status' => 401 ) ); + } + + // Normalize parameter key/values. + $params = $this->normalize_parameters( $params ); + $query_parameters = array(); + foreach ( $params as $param_key => $param_value ) { + if ( is_array( $param_value ) ) { + foreach ( $param_value as $param_key_inner => $param_value_inner ) { + $query_parameters[] = $param_key . '%255B' . $param_key_inner . '%255D%3D' . $param_value_inner; + } + } else { + $query_parameters[] = $param_key . '%3D' . $param_value; // Join with equals sign. + } + } + $query_string = implode( '%26', $query_parameters ); // Join with ampersand. + + $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; + + if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); + + $secret = $user->consumer_secret . '&'; + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); + + if ( ! hash_equals( $signature, $consumer_signature ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), array( 'status' => 401 ) ); + } + + return true; + } + + /** + * Normalize each parameter by assuming each parameter may have already been + * encoded, so attempt to decode, and then re-encode according to RFC 3986. + * + * Note both the key and value is normalized so a filter param like: + * + * 'filter[period]' => 'week' + * + * is encoded to: + * + * 'filter%5Bperiod%5D' => 'week' + * + * This conforms to the OAuth 1.0a spec which indicates the entire query string + * should be URL encoded. + * + * @see rawurlencode() + * @param array $parameters Un-normalized pararmeters. + * @return array Normalized parameters. + */ + private function normalize_parameters( $parameters ) { + $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); + $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); + $parameters = array_combine( $keys, $values ); + + return $parameters; + } + + /** + * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where + * an attacker could attempt to re-send an intercepted request at a later time. + * + * - A timestamp is valid if it is within 15 minutes of now. + * - A nonce is valid if it has not been used within the last 15 minutes. + * + * @param stdClass $user + * @param int $timestamp the unix timestamp for when the request was made + * @param string $nonce a unique (for the given user) 32 alphanumeric string, consumer-generated + * @return bool|WP_Error + */ + private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) { + global $wpdb; + + $valid_window = 15 * 60; // 15 minute window. + + if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces = maybe_unserialize( $user->nonces ); + + if ( empty( $used_nonces ) ) { + $used_nonces = array(); + } + + if ( in_array( $nonce, $used_nonces ) ) { + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), array( 'status' => 401 ) ); + } + + $used_nonces[ $timestamp ] = $nonce; + + // Remove expired nonces. + foreach ( $used_nonces as $nonce_timestamp => $nonce ) { + if ( $nonce_timestamp < ( time() - $valid_window ) ) { + unset( $used_nonces[ $nonce_timestamp ] ); + } + } + + $used_nonces = maybe_serialize( $used_nonces ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'nonces' => $used_nonces ), + array( 'key_id' => $user->key_id ), + array( '%s' ), + array( '%d' ) + ); + + return true; + } + /** * Return the user data for the given consumer_key. * @@ -145,20 +359,20 @@ class WC_REST_Authentication { switch ( $_SERVER['REQUEST_METHOD'] ) { - case 'HEAD': - case 'GET': + case 'HEAD' : + case 'GET' : if ( 'read' !== $permissions && 'read_write' !== $permissions ) { - $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions', 'woocommerce' ), array( 'status' => 401 ) ); + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'woocommerce' ), array( 'status' => 401 ) ); $valid = false; } break; - case 'POST': - case 'PUT': - case 'PATCH': - case 'DELETE': + case 'POST' : + case 'PUT' : + case 'PATCH' : + case 'DELETE' : if ( 'write' !== $permissions && 'read_write' !== $permissions ) { - $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions', 'woocommerce' ), array( 'status' => 401 ) ); + $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'woocommerce' ), array( 'status' => 401 ) ); $valid = false; } break; @@ -166,6 +380,23 @@ class WC_REST_Authentication { return $valid; } + + /** + * Updated API Key last access datetime. + * + * @param int $key_id + */ + private function update_last_access( $key_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->prefix . 'woocommerce_api_keys', + array( 'last_access' => current_time( 'mysql' ) ), + array( 'key_id' => $key_id ), + array( '%s' ), + array( '%d' ) + ); + } } new WC_REST_Authentication(); diff --git a/includes/wc-api-functions.php b/includes/wc-api-functions.php index 91fb1241087..7415a59ad51 100644 --- a/includes/wc-api-functions.php +++ b/includes/wc-api-functions.php @@ -182,3 +182,20 @@ function rest_validate_reports_request_arg( $value, $request, $param ) { return true; } + +/** + * Encodes a value according to RFC 3986. + * Supports multidimensional arrays. + * + * @since 2.6.0 + * @param string|array $value The value to encode. + * @return string|array Encoded values. + */ +function wc_rest_urlencode_rfc3986( $value ) { + if ( is_array( $value ) ) { + return array_map( 'wc_rest_urlencode_rfc3986', $value ); + } else { + // Percent symbols (%) must be double-encoded. + return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); + } +} From 98f4f2110425e96438a6230efedcfb88945d89ad Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 08:47:57 -0300 Subject: [PATCH 086/177] Pass correct unauthorized headers for basic auth --- includes/api/wc-rest-authentication.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index 3d59f08b43b..18de0524730 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -20,6 +20,7 @@ class WC_REST_Authentication { public function __construct() { add_filter( 'determine_current_user', array( $this, 'authenticate' ), 100 ); add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ) ); + add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 ); } /** @@ -397,6 +398,25 @@ class WC_REST_Authentication { array( '%d' ) ); } + + /** + * If the consumer_key and consumer_secret $_GET parameters are NOT provided + * and the Basic auth headers are either not present or the consumer secret does not match the consumer + * key provided, then return the correct Basic headers and an error message. + * + * @param WP_REST_Response $response Current response being served. + * @return WP_REST_Response + */ + public function send_unauthorized_headers( $response ) { + global $wc_rest_authentication_error; + + if ( is_wp_error( $wc_rest_authentication_error ) && is_ssl() ) { + $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field', 'woocommerce' ); + $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); + } + + return $response; + } } new WC_REST_Authentication(); From 8a45c94425398b0238036986e6651f7f7112a46d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 08:51:48 -0300 Subject: [PATCH 087/177] Use perid for all responses --- includes/api/wc-rest-authentication.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index 18de0524730..21aaf0745bc 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -209,7 +209,7 @@ class WC_REST_Authentication { // Sort parameters. if ( ! uksort( $params, 'strcmp' ) ) { - return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), array( 'status' => 401 ) ); + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - failed to sort parameters.', 'woocommerce' ), array( 'status' => 401 ) ); } // Normalize parameter key/values. @@ -229,7 +229,7 @@ class WC_REST_Authentication { $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { - return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), array( 'status' => 401 ) ); + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - signature method is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); @@ -238,7 +238,7 @@ class WC_REST_Authentication { $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { - return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), array( 'status' => 401 ) ); + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); } return true; @@ -289,7 +289,7 @@ class WC_REST_Authentication { $valid_window = 15 * 60; // 15 minute window. if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) { - return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp', 'woocommerce' ), array( 'status' => 401 ) ); + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid timestamp.', 'woocommerce' ), array( 'status' => 401 ) ); } $used_nonces = maybe_unserialize( $user->nonces ); @@ -299,7 +299,7 @@ class WC_REST_Authentication { } if ( in_array( $nonce, $used_nonces ) ) { - return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), array( 'status' => 401 ) ); + return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'woocommerce' ), array( 'status' => 401 ) ); } $used_nonces[ $timestamp ] = $nonce; @@ -411,7 +411,7 @@ class WC_REST_Authentication { global $wc_rest_authentication_error; if ( is_wp_error( $wc_rest_authentication_error ) && is_ssl() ) { - $auth_message = __( 'WooCommerce API. Use a consumer key in the username field and a consumer secret in the password field', 'woocommerce' ); + $auth_message = __( 'WooCommerce API - Use a consumer key in the username field and a consumer secret in the password field.', 'woocommerce' ); $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true ); } From f2728446cab556dfea52307d75fc98a1fb6b26a8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 09:50:54 -0300 Subject: [PATCH 088/177] Improved returned values from authentication --- includes/api/wc-rest-authentication.php | 44 ++++++++++--------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index 21aaf0745bc..e1584951603 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -26,8 +26,8 @@ class WC_REST_Authentication { /** * Authenticate user. * - * @param int $user_id - * @return int + * @param int|false $user_id User ID if one has been determined, false otherwise. + * @return int|false */ public function authenticate( $user_id ) { // Do not authenticate twice! @@ -36,16 +36,10 @@ class WC_REST_Authentication { } if ( is_ssl() ) { - if ( $user_id = $this->perform_basic_authentication() ) { - return $user_id; - } + return $this->perform_basic_authentication(); } else { - if ( $user_id = $this->perform_oauth_authentication() ) { - return $user_id; - } + return $this->perform_oauth_authentication(); } - - return $user_id; } /** @@ -73,13 +67,12 @@ class WC_REST_Authentication { * associated with the given consumer key and confirming the consumer secret * provided is valid. * - * @return int + * @return int|bool */ private function perform_basic_authentication() { global $wc_rest_authentication_error; $user = null; - $user_id = 0; $consumer_key = ''; $consumer_secret = ''; @@ -97,32 +90,32 @@ class WC_REST_Authentication { // Get user data. if ( $consumer_key && $consumer_secret ) { - $user = $this->get_user_data_by_consumer_key( $consumer_key ); - $user_id = $user->user_id; + $user = $this->get_user_data_by_consumer_key( $consumer_key ); } // Abort if don't have an user at this point. if ( empty( $user ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); - return $user_id; + return false; } // Validate user secret. if ( ! hash_equals( $user->consumer_secret, $consumer_secret ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Secret is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); - $user_id = 0; + + return false; } // Check API Key permissions. if ( ! $this->check_permissions( $user->permissions ) ) { - $user_id = 0; + return false; } // Update last access. $this->update_last_access( $user->key_id ); - return $user_id; + return $user->user_id; } /** @@ -138,9 +131,8 @@ class WC_REST_Authentication { * This is because there is no cross-OS function within PHP to get the raw Authorization header. * * @link http://tools.ietf.org/html/rfc5849 for the full spec. - * @since 2.1 - * @return array - * @throws Exception + * + * @return int|bool */ private function perform_oauth_authentication() { global $wc_rest_authentication_error; @@ -150,7 +142,7 @@ class WC_REST_Authentication { // Check for required OAuth parameters. foreach ( $params as $param ) { if ( empty( $_GET[ $param ] ) ) { - return 0; + return false; } } @@ -160,23 +152,23 @@ class WC_REST_Authentication { if ( empty( $user ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); - return $user_id; + return false; } // Perform OAuth validation. $wc_rest_authentication_error = $this->check_oauth_signature( $user, $_GET ); if ( is_wp_error( $wc_rest_authentication_error ) ) { - return 0; + return false; } $wc_rest_authentication_error = $this->check_oauth_timestamp_and_nonce( $user, $_GET['oauth_timestamp'], $_GET['oauth_nonce'] ); if ( is_wp_error( $wc_rest_authentication_error ) ) { - return 0; + return false; } // Check API Key permissions. if ( ! $this->check_permissions( $user->permissions ) ) { - $user_id = 0; + return false; } // Update last access. From 312105b350b9e265110e3f579b606be253b07fbf Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 18 Mar 2016 10:18:44 -0300 Subject: [PATCH 089/177] Improved basic authentication --- includes/api/wc-rest-authentication.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/wc-rest-authentication.php index e1584951603..75c666cf5f3 100644 --- a/includes/api/wc-rest-authentication.php +++ b/includes/api/wc-rest-authentication.php @@ -88,12 +88,13 @@ class WC_REST_Authentication { $consumer_secret = $_SERVER['PHP_AUTH_PW']; } - // Get user data. - if ( $consumer_key && $consumer_secret ) { - $user = $this->get_user_data_by_consumer_key( $consumer_key ); + // Stop if don't have any key. + if ( ! $consumer_key || ! $consumer_secret ) { + return false; } - // Abort if don't have an user at this point. + // Get user data. + $user = $this->get_user_data_by_consumer_key( $consumer_key ); if ( empty( $user ) ) { $wc_rest_authentication_error = new WP_Error( 'woocommerce_rest_authentication_error', __( 'Consumer Key is invalid.', 'woocommerce' ), array( 'status' => 401 ) ); From 7a7856ab7e47626aa88558fdf849b49dda919c9a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 14:53:46 -0300 Subject: [PATCH 090/177] Order notes schema --- .../api/wc-rest-order-notes-controller.php | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index eb8e12b1bd0..cc599f0f3f1 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -40,6 +40,143 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * Register the routes for order notes. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'note' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read order notes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list order notes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create order notes. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'order_note', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_at' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); } } From 2617642d5df39e59fe5d79c02011349b9b213894 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 15:13:33 -0300 Subject: [PATCH 091/177] Added methods to list and get order notes --- .../api/wc-rest-order-notes-controller.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index cc599f0f3f1..1522a945d83 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -140,6 +140,127 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { return true; } + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $id = (int) $request['id']; + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->ID, + 'approve' => 'approve', + 'type' => 'order_note' + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $data = array(); + foreach ( $notes as $note ) { + $order_note = $this->prepare_item_for_response( $note, $request ); + $order_note = $this->prepare_response_for_collection( $order_note ); + $data[] = $order_note; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order_note = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $order_note ); + + return $response; + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => $note->comment_ID, + 'created_at' => wc_rest_api_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $note Delivery order_note object. + * @return array Links for the given order note. + */ + protected function prepare_links( $note ) { + $order_id = (int) $note->comment_post_ID; + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/wc/v1/orders/%d', $order_id ) ), + ), + ); + + return $links; + } + /** * Get the Order Notes schema, conforming to JSON Schema. * From 3fc4effed42ca5beb31739ae95f7f1fe8e125969 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 15:17:35 -0300 Subject: [PATCH 092/177] Added collection params for order notes --- includes/api/wc-rest-order-notes-controller.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 1522a945d83..523eef057b6 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -300,4 +300,15 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } } From 5d5f9950110fa264790147d971076ca41577be2d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 15:31:27 -0300 Subject: [PATCH 093/177] Allow create order notes --- .../api/wc-rest-order-notes-controller.php | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 523eef057b6..55ae363ed26 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -105,7 +105,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! current_user_can( 'publish_shop_orders' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -176,6 +176,53 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { return rest_ensure_response( $data ); } + /** + * Create a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order = wc_get_order( $order ); + + // Create the note. + $note_id = $order->add_order_note( $request['note'], $request['customer_note'] ); + + if ( ! $note_id ) { + return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $note = get_comment( $note_id ); + $this->update_additional_fields_for_object( $note, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $note New order note object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P[\d]+)', $order->id, $this->rest_base ), $note_id ) ) ); + + return $response; + } + /** * Get a single order note. * From 935a784061b5aecce4f3a0becd5a4821271700e0 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 15:34:27 -0300 Subject: [PATCH 094/177] Fixed header location when creating attribute terms --- includes/abstracts/abstract-wc-rest-terms-controller.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 3f45c4f3ed7..3541dfff3d8 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -361,7 +361,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $response = $this->prepare_item_for_response( $term, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); - $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) ); + + $base = '/' . $this->namespace . '/' . $this->rest_base; + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); return $response; } From 3a0bf6c9b879aefb7e01581eb1c47c93a6de5f85 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 22 Mar 2016 15:49:41 -0300 Subject: [PATCH 095/177] Added method to delete order notes --- .../api/wc-rest-order-notes-controller.php | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/wc-rest-order-notes-controller.php index 55ae363ed26..f38176cfa65 100644 --- a/includes/api/wc-rest-order-notes-controller.php +++ b/includes/api/wc-rest-order-notes-controller.php @@ -177,7 +177,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { } /** - * Create a single webhook. + * Create a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response @@ -206,7 +206,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { $this->update_additional_fields_for_object( $note, $request ); /** - * Fires after a single item is created or updated via the REST API. + * Fires after a order note is created or updated via the REST API. * * @param WP_Comment $note New order note object. * @param WP_REST_Request $request Request object. @@ -239,7 +239,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { $note = get_comment( $id ); - if ( empty( $id ) || empty( $note ) ) { + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->ID ) ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); } @@ -249,6 +249,54 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { return $response; } + /** + * Delete a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || 'shop_order' !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + + $result = wp_delete_comment( $note->comment_ID, true );; + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a order note is deleted or trashed via the REST API. + * + * @param WP_Comment $note The deleted or trashed order note. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); + + return $response; + } + /** * Prepare a single order note output for response. * From 7c84066ad7f13a4f816555735195d42ad6ec397d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 23 Mar 2016 08:30:58 -0300 Subject: [PATCH 096/177] Initial order schema --- includes/api/wc-rest-customers-controller.php | 2 +- includes/api/wc-rest-orders-controller.php | 449 +++++++++++++++++- 2 files changed, 448 insertions(+), 3 deletions(-) diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/wc-rest-customers-controller.php index 67e0d276dd7..ef0779f2614 100644 --- a/includes/api/wc-rest-customers-controller.php +++ b/includes/api/wc-rest-customers-controller.php @@ -608,7 +608,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { } /** - * Get the Customer's schema, conforming to JSON Schema + * Get the Customer's schema, conforming to JSON Schema. * * @return array */ diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index e301e75e401..369a79f9f28 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -47,7 +47,47 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * Register the routes for orders. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -55,10 +95,415 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'args' => array( 'force' => array( 'default' => false, - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read orders. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list orders.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create orders. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_at' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated_at' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'completed_at' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $order_statuses, + 'context' => array( 'view', 'edit' ), + ), + 'currency' => array( + 'description' => __( 'Currency in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Order total.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Order subtotal.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_line_items_quantity' => array( + 'description' => __( 'Total of order items.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Order tax total.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_shipping' => array( + 'description' => __( 'Order shipping total.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Order cart tax.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Order shipping tax.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_discount' => array( + 'description' => __( 'Order total discount.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_methods' => array( + 'description' => __( 'Text list of the shipping methods used in the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'payment_details' => array( + 'description' => __( 'Payment details.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'method_id' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'paid' => array( + 'description' => __( 'Shows/define if the order is paid using this payment method.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Transaction ID, an optional field to set the transacion ID when complate one payment.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'billing_address' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_address' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'note' => array( + 'description' => __( 'Customer order notes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip' => array( + 'description' => __( 'Customer IP address.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'Customer User-Agent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'Customer ID (user ID).', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + + ), + ), + 'tax_lines' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + + ), + ), + 'fee_lines' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + ), ), ), - ) ); + ); + + return $this->add_additional_fields_schema( $schema ); } } From efe4b765596d6fafc812a54c050685c787bcf4fe Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 23 Mar 2016 09:46:34 -0300 Subject: [PATCH 097/177] New order schema --- includes/api/wc-rest-orders-controller.php | 188 +++++++++++---------- 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 369a79f9f28..624adcc7716 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -197,35 +197,10 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'number' => array( - 'description' => __( 'Order number.', 'woocommerce' ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'order_key' => array( - 'description' => __( 'Order key.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'created_at' => array( - 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'updated_at' => array( - 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'completed_at' => array( - 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'status' => array( 'description' => __( 'Order status.', 'woocommerce' ), @@ -234,95 +209,91 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'enum' => $order_statuses, 'context' => array( 'view', 'edit' ), ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), 'currency' => array( - 'description' => __( 'Currency in ISO format.', 'woocommerce' ), + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( get_woocommerce_currencies() ), 'context' => array( 'view', 'edit' ), ), - 'total' => array( - 'description' => __( 'Order total.', 'woocommerce' ), - 'type' => 'float', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'subtotal' => array( - 'description' => __( 'Order subtotal.', 'woocommerce' ), - 'type' => 'float', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'total_line_items_quantity' => array( - 'description' => __( 'Total of order items.', 'woocommerce' ), + 'version' => array( + 'description' => __( 'Version of WooCommerce when the order was made.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'total_tax' => array( - 'description' => __( 'Order tax total.', 'woocommerce' ), + 'prices_include_tax' => array( + 'description' => __( 'Shows if the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'total_shipping' => array( - 'description' => __( 'Order shipping total.', 'woocommerce' ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'cart_tax' => array( - 'description' => __( 'Order cart tax.', 'woocommerce' ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax' => array( - 'description' => __( 'Order shipping tax.', 'woocommerce' ), + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'total_discount' => array( - 'description' => __( 'Order total discount.', 'woocommerce' ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'shipping_methods' => array( - 'description' => __( 'Text list of the shipping methods used in the order.', 'woocommerce' ), - 'type' => 'string', + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'payment_details' => array( - 'description' => __( 'Payment details.', 'woocommerce' ), - 'type' => 'object', + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'float', 'context' => array( 'view', 'edit' ), 'readonly' => true, - 'properties' => array( - 'method_id' => array( - 'description' => __( 'Payment method ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'method_title' => array( - 'description' => __( 'Payment method title.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'paid' => array( - 'description' => __( 'Shows/define if the order is paid using this payment method.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - ), - 'transaction_id' => array( - 'description' => __( 'Transaction ID, an optional field to set the transacion ID when complate one payment.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - ), - ), ), - 'billing_address' => array( + 'billing' => array( 'description' => __( 'Billing address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), @@ -368,7 +339,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), ), 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), @@ -385,7 +356,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), ), ), - 'shipping_address' => array( + 'shipping' => array( 'description' => __( 'Shipping address.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), @@ -431,34 +402,67 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), ), 'country' => array( - 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ), ), - 'note' => array( - 'description' => __( 'Customer order notes.', 'woocommerce' ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'customer_ip' => array( - 'description' => __( 'Customer IP address.', 'woocommerce' ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'customer_user_agent' => array( - 'description' => __( 'Customer User-Agent.', 'woocommerce' ), + 'description' => __( 'User agent of the customer.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'customer_id' => array( - 'description' => __( 'Customer ID (user ID).', 'woocommerce' ), - 'type' => 'integer', - 'default' => 0, + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid' => array( + 'description' => __( "The date the order has been paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, ), 'line_items' => array( 'description' => __( 'Last order data.', 'woocommerce' ), From 91dd99e472ba0906aac605b7ada91a9c0990a3ff Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 23 Mar 2016 19:19:08 -0300 Subject: [PATCH 098/177] Added methods to list and get orders --- includes/api/wc-rest-orders-controller.php | 678 ++++++++++++++++++- includes/api/wc-rest-webhooks-controller.php | 40 +- 2 files changed, 674 insertions(+), 44 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 624adcc7716..349104c39bd 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -43,6 +43,13 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'shop_order'; + /** + * Initialize orders actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + /** * Register the routes for orders. */ @@ -175,17 +182,330 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } /** - * Get the Order's schema, conforming to JSON Schema. + * Prepare a single coupon output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + global $wpdb; + + $order = wc_get_order( $post ); + $dp = ! empty( $request['dp'] ) ? intval( $request['dp'] ) : 2; + + $data = array( + 'id' => $order->id, + 'parent_id' => $post->post_parent, + 'status' => $order->get_status(), + 'order_key' => $order->order_key, + 'currency' => $order->get_order_currency(), + 'version' => $order->order_version, + 'prices_include_tax' => $order->prices_include_tax, + 'date_created' => wc_rest_api_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_api_prepare_date_response( $post->post_modified_gmt ), + 'customer_id' => $order->get_user_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->cart_discount_tax, $dp ), + 'shipping_total' => wc_format_decimal( $order->get_total_shipping(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'billing' => array(), + 'shipping' => array(), + 'payment_method' => $order->payment_method, + 'payment_method_title' => $order->payment_method_title, + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->customer_ip_address, + 'customer_user_agent' => $order->customer_user_agent, + 'created_via' => $order->created_via, + 'customer_note' => $order->customer_note, + 'date_completed' => wc_rest_api_prepare_date_response( $order->completed_date, true ), + 'date_paid' => $order->paid_date, + 'cart_hash' => $order->cart_hash, + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $order->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['value'], + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + // if ( in_array( 'products', $expand ) ) { + // $_product_data = WC()->api->WC_API_Products->get_product( $product_id ); + + // if ( isset( $_product_data['product'] ) ) { + // $line_item['product_data'] = $_product_data['product']; + // } + // } + + $data['line_items'][] = $line_item; + } + + // Add taxes. + foreach ( $order->get_items( 'tax' ) as $key => $tax ) { + $tax_line = array( + 'id' => $key, + 'rate_code' => $tax['name'], + 'rate_id' => $tax['rate_id'], + 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], + 'compound' => (bool) $tax['compound'], + 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), + 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), + ); + + // if ( in_array( 'taxes', $expand ) ) { + // $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); + + // if ( isset( $_rate_data['tax'] ) ) { + // $tax_line['rate_data'] = $_rate_data['tax']; + // } + // } + + $data['tax_lines'][] = $tax_line; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $shipping_line = array( + 'id' => $shipping_item_id, + 'method_title' => $shipping_item['name'], + 'method_id' => $shipping_item['method_id'], + 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), + 'total_tax' => wc_format_decimal( '', $dp ), + 'taxes' => array(), + ); + + $shipping_taxes = maybe_unserialize( $shipping_item['taxes'] ); + + if ( ! empty( $shipping_taxes ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes ), $dp ); + + foreach ( $shipping_taxes as $tax_rate_id => $tax ) { + $shipping_line['taxes'][] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + ); + } + } + + $data['shipping_lines'][] = $shipping_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $fee_line = array( + 'id' => $fee_item_id, + 'name' => $fee_item['name'], + 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', + 'tax_status' => 'taxable', + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + 'taxes' => array(), + ); + + $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); + if ( isset( $fee_line_taxes['total'] ) ) { + $fee_tax = array(); + + foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $fee_line['taxes'] = array_values( $fee_tax ); + } + + $data['fee_lines'][] = $fee_line; + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item['name'], + 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), + 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), + ); + + // if ( in_array( 'coupons', $expand ) ) { + // $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item['name'] ); + + // if ( isset( $_coupon_data['coupon'] ) ) { + // $coupon_line['coupon_data'] = $_coupon_data['coupon']; + // } + // } + + $data['coupon_lines'][] = $coupon_line; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $order ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order $order Comment object. + * @return array Links for the given order. + */ + protected function prepare_links( $order ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $order->get_user_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), + ); + } + + if ( 0 !== (int) $order->post->post_parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->post->post_parent ) ), + ); + } + + return $links; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + return $args; + } + + /** + * Get order statuses. * * @return array */ - public function get_item_schema() { + protected function get_order_statuses() { $order_statuses = array(); foreach ( array_keys( wc_get_order_statuses() ) as $status ) { $order_statuses[] = str_replace( 'wc-', '', $status ); } + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->post_type, @@ -206,7 +526,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Order status.', 'woocommerce' ), 'type' => 'string', 'default' => 'pending', - 'enum' => $order_statuses, + 'enum' => $this->get_order_statuses(), 'context' => array( 'view', 'edit' ), ), 'order_key' => array( @@ -253,49 +573,49 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), 'discount_total' => array( 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'discount_tax' => array( 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_total' => array( 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'shipping_tax' => array( 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'cart_tax' => array( 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total' => array( 'description' => __( 'Grand total.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Sum of all taxes.', 'woocommerce' ), - 'type' => 'float', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing' => array( 'description' => __( 'Billing address.', 'woocommerce' ), - 'type' => 'object', + 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( @@ -358,7 +678,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), 'shipping' => array( 'description' => __( 'Shipping address.', 'woocommerce' ), - 'type' => 'object', + 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( 'first_name' => array( @@ -469,15 +789,127 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( - - ), - ), - 'shipping_lines' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), ), ), 'tax_lines' => array( @@ -486,7 +918,104 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => array( - + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), ), ), 'fee_lines' => array( @@ -494,7 +1023,67 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( - + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), ), ), 'coupon_lines' => array( @@ -502,7 +1091,28 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( - + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), ), ), ), @@ -510,4 +1120,24 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } } diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/wc-rest-webhooks-controller.php index b06e336cd3f..6f5930973fe 100644 --- a/includes/api/wc-rest-webhooks-controller.php +++ b/includes/api/wc-rest-webhooks-controller.php @@ -517,26 +517,6 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return $args; } - /** - * Get the query params for collections of attachments. - * - * @return array - */ - public function get_collection_params() { - $params = parent::get_collection_params(); - - $params['status'] = array( - 'default' => 'all', - 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array( 'all', 'active', 'paused', 'disabled' ), - 'sanitize_callback' => 'sanitize_key', - 'validate_callback' => 'rest_validate_request_arg', - ); - - return $params; - } - /** * Get the Webhook's schema, conforming to JSON Schema. * @@ -623,4 +603,24 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } } From 1bbbbf70f5d0b0339262863a4da3a7f20be9f6d1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 24 Mar 2016 17:25:10 -0300 Subject: [PATCH 099/177] Added initial methods to create an order --- includes/api/wc-rest-orders-controller.php | 117 ++++++++++++++++++--- 1 file changed, 105 insertions(+), 12 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 349104c39bd..1bcc87b4187 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -65,17 +65,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( - 'email' => array( - 'required' => true, - ), - 'username' => array( - 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), - ), - 'password' => array( - 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), - ), - ) ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -132,7 +122,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! current_user_can( 'publish_shop_orders' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -485,6 +475,109 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { return $args; } + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + wc_transaction_query( 'start' ); + + try { + $order = wc_create_order( array( + 'status' => $request['status'], + 'customer_id' => $request['customer_id'], + 'customer_note' => $request['customer_note'], + 'created_via' => 'rest-api', + ) ); + + if ( is_wp_error( $order ) ) { + throw new Exception( sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // Set addresses. + if ( ! empty( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( ! empty( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + wc_transaction_query( 'commit' ); + + return $order->id; + } catch ( Exception $e ) { + wc_transaction_query( 'rollback' ); + + return new WP_Error( "woocommerce_rest_{$this->post_type}_create_error", $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + $fields = $order->get_address( $type ); + + foreach ( array_keys( $fields ) as $field ) { + if ( isset( $posted[ $field ] ) ) { + $fields[ $field ] = $posted[ $field ]; + } + } + + // Set address. + $order->set_address( $fields, $type ); + + // Update user meta. + if ( $order->get_user_id() ) { + foreach ( $fields as $key => $value ) { + update_user_meta( $order->get_user_id(), $type . '_' . $key, $value ); + } + } + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_id = $this->create_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + /** * Get order statuses. * From 8bc11ddebd45c101881fe42eb928adb34eb74ef7 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 24 Mar 2016 17:43:14 -0300 Subject: [PATCH 100/177] Save payment and currency when creating orders --- includes/api/wc-rest-orders-controller.php | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 1bcc87b4187..57f644c1c67 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -504,6 +504,26 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { $this->update_address( $order, $request['shipping'], 'shipping' ); } + // Set currency. + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); + } + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // TODO + // Set order meta. + // if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + // $this->set_order_meta( $order->id, $data['order_meta'] ); + // } + wc_transaction_query( 'commit' ); return $order->id; @@ -631,6 +651,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'currency' => array( 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), 'type' => 'string', + 'default' => get_woocommerce_currency(), 'enum' => array_keys( get_woocommerce_currencies() ), 'context' => array( 'view', 'edit' ), ), @@ -831,6 +852,13 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + 'writeonly' => true, + ), 'transaction_id' => array( 'description' => __( 'Unique transaction ID.', 'woocommerce' ), 'type' => 'boolean', From c4c7ef6da98e2feab99ad8e782ea35e51f4a91df Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 24 Mar 2016 17:55:51 -0300 Subject: [PATCH 101/177] Added method to add metadata --- includes/api/wc-rest-orders-controller.php | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 57f644c1c67..68c58d20797 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -518,11 +518,10 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { $order->payment_complete( $request['transaction_id'] ); } - // TODO - // Set order meta. - // if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { - // $this->set_order_meta( $order->id, $data['order_meta'] ); - // } + // Set meta data. + if ( ! empty( $data['meta_data'] ) && is_array( $data['meta_data'] ) ) { + $this->update_meta_data( $order->id, $data['meta_data'] ); + } wc_transaction_query( 'commit' ); @@ -561,6 +560,23 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } } + /** + * Helper method to add/update meta data, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @param WC_Order $order Order data. + * @param array $meta_data Meta data in array( 'meta_key' => 'meta_value' ) format. + */ + protected function update_meta_data( $order_id, $meta_data ) { + foreach ( $meta_data as $meta_key => $meta_value ) { + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + /** * Create a single item. * From 855ff7e6fef5eb059a1e01e06f68e36f7706abf5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 24 Mar 2016 18:04:27 -0300 Subject: [PATCH 102/177] Check if customer exists and improved address and meta data --- includes/api/wc-rest-orders-controller.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 68c58d20797..7a8cd4bcd5c 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -485,6 +485,11 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { wc_transaction_query( 'start' ); try { + // Make sure customer exists. + if ( 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new Exception( __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + $order = wc_create_order( array( 'status' => $request['status'], 'customer_id' => $request['customer_id'], @@ -497,10 +502,10 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } // Set addresses. - if ( ! empty( $request['billing'] ) ) { + if ( is_array( $request['billing'] ) ) { $this->update_address( $order, $request['billing'], 'billing' ); } - if ( ! empty( $request['shipping'] ) ) { + if ( is_array( $request['shipping'] ) ) { $this->update_address( $order, $request['shipping'], 'shipping' ); } @@ -519,8 +524,8 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } // Set meta data. - if ( ! empty( $data['meta_data'] ) && is_array( $data['meta_data'] ) ) { - $this->update_meta_data( $order->id, $data['meta_data'] ); + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); } wc_transaction_query( 'commit' ); From 194b40ecb9502abfeef3cc68c96327e28078f45a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 09:22:44 -0300 Subject: [PATCH 103/177] Created exception class --- includes/api/class-wc-rest-exception.php | 19 +++++++++++++++++++ includes/class-wc-api.php | 6 ++++++ 2 files changed, 25 insertions(+) create mode 100644 includes/api/class-wc-rest-exception.php diff --git a/includes/api/class-wc-rest-exception.php b/includes/api/class-wc-rest-exception.php new file mode 100644 index 00000000000..a059493dba1 --- /dev/null +++ b/includes/api/class-wc-rest-exception.php @@ -0,0 +1,19 @@ + Date: Mon, 28 Mar 2016 09:22:54 -0300 Subject: [PATCH 104/177] Allow create orders --- includes/api/wc-rest-orders-controller.php | 305 ++++++++++++++++++++- 1 file changed, 291 insertions(+), 14 deletions(-) diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/wc-rest-orders-controller.php index 7a8cd4bcd5c..268fe43fc0b 100644 --- a/includes/api/wc-rest-orders-controller.php +++ b/includes/api/wc-rest-orders-controller.php @@ -487,7 +487,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { try { // Make sure customer exists. if ( 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { - throw new Exception( __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); } $order = wc_create_order( array( @@ -498,7 +498,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ) ); if ( is_wp_error( $order ) ) { - throw new Exception( sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order', sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); } // Set addresses. @@ -512,6 +512,26 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { // Set currency. update_post_meta( $order->id, '_order_currency', $request['currency'] ); + // Set lines. + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( is_array( $request[ $line ] ) ) { + foreach ( $request[ $line ] as $item ) { + $set_item = 'set_' . $line_type; + $new_item = $this->$set_item( $order, $item, 'create' ); + } + } + } + + // Calculate totals and set them. + $order->calculate_totals(); + // Set payment method. if ( ! empty( $request['payment_method'] ) ) { update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); @@ -531,10 +551,10 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { wc_transaction_query( 'commit' ); return $order->id; - } catch ( Exception $e ) { + } catch ( WC_REST_Exception $e ) { wc_transaction_query( 'rollback' ); - return new WP_Error( "woocommerce_rest_{$this->post_type}_create_error", $e->getMessage(), array( 'status' => $e->getCode() ) ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); } } @@ -565,6 +585,270 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } } + /** + * Create or update a line item. + * + * @param WC_Order $order Order data. + * @param array $item Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_line_item( $order, $item, $action = 'create' ) { + $creating = 'create' === $action; + $item_args = array(); + + // Product is always required. + if ( empty( $item['product_id'] ) && empty( $item['sku'] ) && empty( $item['variation_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $item['product_id'] ) ) { + $product_id = (int) $item['product_id']; + } else if ( ! empty( $item['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $item['sku'] ); + } else if ( ! empty( $item['variation_id'] ) ) { + $product_id = (int) $item['variation_id']; + } + + // When updating, ensure product ID provided matches. + if ( 'update' === $action && ! empty( $item['id'] ) ) { + $item_product_id = (int) wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = (int) wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $product_id !== $item_product_id && $product_id !== $item_variation_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or variation ID provided does not match this line item.', 'woocommerce' ), 400 ); + } + } + + $product = wc_get_product( $product_id ); + + // Must be a valid WC_Product. + if ( ! is_object( $product ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // Quantity must be positive float. + if ( isset( $item['quantity'] ) && 0 >= floatval( $item['quantity'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // Quantity is required when creating. + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // Get variation attributes. + if ( method_exists( $product, 'get_variation_attributes' ) ) { + $item_args['variation'] = $product->get_variation_attributes(); + } + + // Quantity. + if ( isset( $item['quantity'] ) ) { + $item_args['qty'] = $item['quantity']; + } + + // Total. + if ( isset( $item['total'] ) ) { + $item_args['totals']['total'] = floatval( $item['total'] ); + } + + // Total tax. + if ( isset( $item['total_tax'] ) ) { + $item_args['totals']['tax'] = floatval( $item['total_tax'] ); + } + + // Subtotal. + if ( isset( $item['subtotal'] ) ) { + $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); + } + + // Subtotal tax. + if ( isset( $item['subtotal_tax'] ) ) { + $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); + } + + if ( $creating ) { + $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } else { + $item_id = $order->update_product( $item['id'], $product, $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_line_item', __( 'Cannot update line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order shipping method. + * + * @param WC_Order $order Order data. + * @param array $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_shipping( $order, $shipping, $action ) { + // Total must be a positive float. + if ( ! empty( $shipping['total'] ) && 0 > floatval( $shipping['total'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Method ID is required. + if ( empty( $shipping['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + + $shipping_id = $order->add_shipping( $rate ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_shipping', __( 'Cannot create shipping method, try again.', 'woocommerce' ), 500 ); + } + + } else { + $shipping_args = array(); + + if ( isset( $shipping['method_id'] ) ) { + $shipping_args['method_id'] = $shipping['method_id']; + } + + if ( isset( $shipping['method_title'] ) ) { + $shipping_args['method_title'] = $shipping['method_title']; + } + + if ( isset( $shipping['total'] ) ) { + $shipping_args['cost'] = floatval( $shipping['total'] ); + } + + $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee. + * + * @param WC_Order $order Order data. + * @param array $fee Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_fee( $order, $fee, $action ) { + if ( 'create' === $action ) { + + // Fee name is required. + if ( empty( $fee['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + + $fee_data = new stdClass(); + $fee_data->id = sanitize_title( $fee['name'] ); + $fee_data->name = $fee['name']; + $fee_data->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; + $fee_data->taxable = false; + $fee_data->tax = 0; + $fee_data->tax_data = array(); + $fee_data->tax_class = ''; + + // If taxable, tax class and total are required. + if ( isset( $fee['tax_status'] ) && 'taxable' === $fee['tax_status'] ) { + + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $fee_data->taxable = true; + $fee_data->tax_class = $fee['tax_class']; + + if ( isset( $fee['total_tax'] ) ) { + $fee_data->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; + } + } + + $fee_id = $order->add_fee( $fee_data ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_fee', __( 'Cannot create fee, try again.', 'woocommerce' ), 500 ); + } + + } else { + $fee_args = array(); + + if ( isset( $fee['name'] ) ) { + $fee_args['name'] = $fee['name']; + } + + if ( isset( $fee['tax_class'] ) ) { + $fee_args['tax_class'] = $fee['tax_class']; + } + + if ( isset( $fee['total'] ) ) { + $fee_args['line_total'] = floatval( $fee['total'] ); + } + + if ( isset( $fee['total_tax'] ) ) { + $fee_args['line_tax'] = floatval( $fee['total_tax'] ); + } + + $fee_id = $order->update_fee( $fee['id'], $fee_args ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon. + * + * @param WC_Order $order Order data. + * @param array $coupon Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_coupon( $order, $coupon, $action ) { + // Coupon discount must be positive float. + if ( isset( $coupon['discount'] ) && 0 > floatval( $coupon['discount'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_total', __( 'Coupon discount must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Coupon code is required. + if ( empty( $coupon['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $coupon_id = $order->add_coupon( $coupon['code'], floatval( $coupon['discount'] ) ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order_coupon', __( 'Cannot create coupon, try again.', 'woocommerce' ), 500 ); + } + + } else { + $coupon_args = array(); + + if ( isset( $coupon['code'] ) ) { + $coupon_args['code'] = $coupon['code']; + } + + if ( isset( $coupon['discount'] ) ) { + $coupon_args['discount_amount'] = floatval( $coupon['discount'] ); + } + + $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + /** * Helper method to add/update meta data, with two restrictions: * @@ -598,6 +882,9 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { return $order_id; } + // Clear transients. + wc_delete_shop_order_transients( $order_id ); + $post = get_post( $order_id ); $this->update_additional_fields_for_object( $post, $request ); @@ -980,25 +1267,21 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'subtotal_tax' => array( 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'total' => array( 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line total tax.', 'woocommerce' ), @@ -1119,7 +1402,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Shipping method name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'method_id' => array( 'description' => __( 'Shipping method ID.', 'woocommerce' ), @@ -1130,7 +1412,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Line total (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), @@ -1175,7 +1456,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Fee name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'tax_class' => array( 'description' => __( 'Tax class of fee.', 'woocommerce' ), @@ -1186,19 +1466,16 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'description' => __( 'Tax status of fee.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'total' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'total_tax' => array( 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), 'taxes' => array( 'description' => __( 'Line total tax.', 'woocommerce' ), From 242b3620208149ab30c8730c658bd8758ccbbef1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 09:25:50 -0300 Subject: [PATCH 105/177] Fixed file names --- ...n.php => class-wc-rest-authentication.php} | 0 ...p => class-wc-rest-coupons-controller.php} | 0 ...=> class-wc-rest-customers-controller.php} | 0 ... class-wc-rest-order-notes-controller.php} | 0 ...lass-wc-rest-order-refunds-controller.php} | 0 ...hp => class-wc-rest-orders-controller.php} | 0 ...st-product-attribute-terms-controller.php} | 0 ...wc-rest-product-attributes-controller.php} | 0 ...wc-rest-product-categories-controller.php} | 0 ...t-product-shipping-classes-controller.php} | 0 ...class-wc-rest-product-tags-controller.php} | 0 ... => class-wc-rest-products-controller.php} | 0 ...class-wc-rest-report-sales-controller.php} | 0 ...wc-rest-report-top-sellers-controller.php} | 0 ...p => class-wc-rest-reports-controller.php} | 0 ... class-wc-rest-tax-classes-controller.php} | 0 ...php => class-wc-rest-taxes-controller.php} | 0 ...p => class-wc-rest-webhook-deliveries.php} | 0 ... => class-wc-rest-webhooks-controller.php} | 0 includes/class-wc-api.php | 38 +++++++++---------- 20 files changed, 19 insertions(+), 19 deletions(-) rename includes/api/{wc-rest-authentication.php => class-wc-rest-authentication.php} (100%) rename includes/api/{wc-rest-coupons-controller.php => class-wc-rest-coupons-controller.php} (100%) rename includes/api/{wc-rest-customers-controller.php => class-wc-rest-customers-controller.php} (100%) rename includes/api/{wc-rest-order-notes-controller.php => class-wc-rest-order-notes-controller.php} (100%) rename includes/api/{wc-rest-order-refunds-controller.php => class-wc-rest-order-refunds-controller.php} (100%) rename includes/api/{wc-rest-orders-controller.php => class-wc-rest-orders-controller.php} (100%) rename includes/api/{wc-rest-product-attribute-terms-controller.php => class-wc-rest-product-attribute-terms-controller.php} (100%) rename includes/api/{wc-rest-product-attributes-controller.php => class-wc-rest-product-attributes-controller.php} (100%) rename includes/api/{wc-rest-product-categories-controller.php => class-wc-rest-product-categories-controller.php} (100%) rename includes/api/{wc-rest-product-shipping-classes-controller.php => class-wc-rest-product-shipping-classes-controller.php} (100%) rename includes/api/{wc-rest-product-tags-controller.php => class-wc-rest-product-tags-controller.php} (100%) rename includes/api/{wc-rest-products-controller.php => class-wc-rest-products-controller.php} (100%) rename includes/api/{wc-rest-report-sales-controller.php => class-wc-rest-report-sales-controller.php} (100%) rename includes/api/{wc-rest-report-top-sellers-controller.php => class-wc-rest-report-top-sellers-controller.php} (100%) rename includes/api/{wc-rest-reports-controller.php => class-wc-rest-reports-controller.php} (100%) rename includes/api/{wc-rest-tax-classes-controller.php => class-wc-rest-tax-classes-controller.php} (100%) rename includes/api/{wc-rest-taxes-controller.php => class-wc-rest-taxes-controller.php} (100%) rename includes/api/{wc-rest-webhook-deliveries.php => class-wc-rest-webhook-deliveries.php} (100%) rename includes/api/{wc-rest-webhooks-controller.php => class-wc-rest-webhooks-controller.php} (100%) diff --git a/includes/api/wc-rest-authentication.php b/includes/api/class-wc-rest-authentication.php similarity index 100% rename from includes/api/wc-rest-authentication.php rename to includes/api/class-wc-rest-authentication.php diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php similarity index 100% rename from includes/api/wc-rest-coupons-controller.php rename to includes/api/class-wc-rest-coupons-controller.php diff --git a/includes/api/wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php similarity index 100% rename from includes/api/wc-rest-customers-controller.php rename to includes/api/class-wc-rest-customers-controller.php diff --git a/includes/api/wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php similarity index 100% rename from includes/api/wc-rest-order-notes-controller.php rename to includes/api/class-wc-rest-order-notes-controller.php diff --git a/includes/api/wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php similarity index 100% rename from includes/api/wc-rest-order-refunds-controller.php rename to includes/api/class-wc-rest-order-refunds-controller.php diff --git a/includes/api/wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php similarity index 100% rename from includes/api/wc-rest-orders-controller.php rename to includes/api/class-wc-rest-orders-controller.php diff --git a/includes/api/wc-rest-product-attribute-terms-controller.php b/includes/api/class-wc-rest-product-attribute-terms-controller.php similarity index 100% rename from includes/api/wc-rest-product-attribute-terms-controller.php rename to includes/api/class-wc-rest-product-attribute-terms-controller.php diff --git a/includes/api/wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php similarity index 100% rename from includes/api/wc-rest-product-attributes-controller.php rename to includes/api/class-wc-rest-product-attributes-controller.php diff --git a/includes/api/wc-rest-product-categories-controller.php b/includes/api/class-wc-rest-product-categories-controller.php similarity index 100% rename from includes/api/wc-rest-product-categories-controller.php rename to includes/api/class-wc-rest-product-categories-controller.php diff --git a/includes/api/wc-rest-product-shipping-classes-controller.php b/includes/api/class-wc-rest-product-shipping-classes-controller.php similarity index 100% rename from includes/api/wc-rest-product-shipping-classes-controller.php rename to includes/api/class-wc-rest-product-shipping-classes-controller.php diff --git a/includes/api/wc-rest-product-tags-controller.php b/includes/api/class-wc-rest-product-tags-controller.php similarity index 100% rename from includes/api/wc-rest-product-tags-controller.php rename to includes/api/class-wc-rest-product-tags-controller.php diff --git a/includes/api/wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php similarity index 100% rename from includes/api/wc-rest-products-controller.php rename to includes/api/class-wc-rest-products-controller.php diff --git a/includes/api/wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php similarity index 100% rename from includes/api/wc-rest-report-sales-controller.php rename to includes/api/class-wc-rest-report-sales-controller.php diff --git a/includes/api/wc-rest-report-top-sellers-controller.php b/includes/api/class-wc-rest-report-top-sellers-controller.php similarity index 100% rename from includes/api/wc-rest-report-top-sellers-controller.php rename to includes/api/class-wc-rest-report-top-sellers-controller.php diff --git a/includes/api/wc-rest-reports-controller.php b/includes/api/class-wc-rest-reports-controller.php similarity index 100% rename from includes/api/wc-rest-reports-controller.php rename to includes/api/class-wc-rest-reports-controller.php diff --git a/includes/api/wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php similarity index 100% rename from includes/api/wc-rest-tax-classes-controller.php rename to includes/api/class-wc-rest-tax-classes-controller.php diff --git a/includes/api/wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php similarity index 100% rename from includes/api/wc-rest-taxes-controller.php rename to includes/api/class-wc-rest-taxes-controller.php diff --git a/includes/api/wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php similarity index 100% rename from includes/api/wc-rest-webhook-deliveries.php rename to includes/api/class-wc-rest-webhook-deliveries.php diff --git a/includes/api/wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php similarity index 100% rename from includes/api/wc-rest-webhooks-controller.php rename to includes/api/class-wc-rest-webhooks-controller.php diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 53345d8ea4c..bee17234ccc 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -370,7 +370,7 @@ class WC_API { include_once( 'api/class-wc-rest-exception.php' ); // Authentication. - include_once( 'api/wc-rest-authentication.php' ); + include_once( 'api/class-wc-rest-authentication.php' ); // WP-API classes and functions. include_once( 'vendor/wp-api-functions.php' ); @@ -383,24 +383,24 @@ class WC_API { include_once( 'abstracts/abstract-wc-rest-terms-controller.php' ); // REST API controllers. - include_once( 'api/wc-rest-coupons-controller.php' ); - include_once( 'api/wc-rest-customers-controller.php' ); - include_once( 'api/wc-rest-order-notes-controller.php' ); - include_once( 'api/wc-rest-order-refunds-controller.php' ); - include_once( 'api/wc-rest-orders-controller.php' ); - include_once( 'api/wc-rest-product-attribute-terms-controller.php' ); - include_once( 'api/wc-rest-product-attributes-controller.php' ); - include_once( 'api/wc-rest-product-categories-controller.php' ); - include_once( 'api/wc-rest-product-shipping-classes-controller.php' ); - include_once( 'api/wc-rest-product-tags-controller.php' ); - include_once( 'api/wc-rest-products-controller.php' ); - include_once( 'api/wc-rest-report-sales-controller.php' ); - include_once( 'api/wc-rest-report-top-sellers-controller.php' ); - include_once( 'api/wc-rest-reports-controller.php' ); - include_once( 'api/wc-rest-tax-classes-controller.php' ); - include_once( 'api/wc-rest-taxes-controller.php' ); - include_once( 'api/wc-rest-webhook-deliveries.php' ); - include_once( 'api/wc-rest-webhooks-controller.php' ); + include_once( 'api/class-wc-rest-coupons-controller.php' ); + include_once( 'api/class-wc-rest-customers-controller.php' ); + include_once( 'api/class-wc-rest-order-notes-controller.php' ); + include_once( 'api/class-wc-rest-order-refunds-controller.php' ); + include_once( 'api/class-wc-rest-orders-controller.php' ); + include_once( 'api/class-wc-rest-product-attribute-terms-controller.php' ); + include_once( 'api/class-wc-rest-product-attributes-controller.php' ); + include_once( 'api/class-wc-rest-product-categories-controller.php' ); + include_once( 'api/class-wc-rest-product-shipping-classes-controller.php' ); + include_once( 'api/class-wc-rest-product-tags-controller.php' ); + include_once( 'api/class-wc-rest-products-controller.php' ); + include_once( 'api/class-wc-rest-report-sales-controller.php' ); + include_once( 'api/class-wc-rest-report-top-sellers-controller.php' ); + include_once( 'api/class-wc-rest-reports-controller.php' ); + include_once( 'api/class-wc-rest-tax-classes-controller.php' ); + include_once( 'api/class-wc-rest-taxes-controller.php' ); + include_once( 'api/class-wc-rest-webhook-deliveries.php' ); + include_once( 'api/class-wc-rest-webhooks-controller.php' ); } /** From 9738ee04c2d3c71f7ada86f93a9f3db268e2db14 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 16:04:24 -0300 Subject: [PATCH 106/177] Update orders --- .../api/class-wc-rest-orders-controller.php | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 268fe43fc0b..634d33fc979 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -906,6 +906,202 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { return $response; } + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = 'set_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->id ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @param WP_Post $post Post data. + * @return int|WP_Error + */ + protected function update_order( $request, $post ) { + try { + $update_totals = false; + $order = wc_get_order( $post ); + $order_args = array( 'order_id' => $order->id ); + + // Customer note. + if ( isset( $request['customer_note'] ) ) { + $order_args['customer_note'] = $request['customer_note']; + } + + // Customer ID. + if ( isset( $request['customer_id'] ) && $request['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->id, '_customer_user', $request['customer_id'] ); + } + + // Update addresses. + if ( is_array( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( is_array( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( isset( $request[ $line ] ) && is_array( $request[ $line ] ) ) { + $update_totals = true; + foreach ( $request[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID is required.', 'woocommerce' ), 400 ); + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); + } + if ( $order->needs_payment() && isset( $request['set_paid'] ) && true === $request['set_paid'] ) { + $order->payment_complete( ! empty( $request['transaction_id'] ) ? $request['transaction_id'] : '' ); + } + + // Set order currency. + if ( isset( $request['currency'] ) ) { + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update meta data. + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $request['status'] ) ) { + $order->update_status( $request['status'], isset( $request['status_note'] ) ? $request['status_note'] : '' ); + } + + return $order->id; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update a single order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request, $post ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Clear transients. + wc_delete_shop_order_transients( $order_id ); + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + } + /** * Get order statuses. * From 2cd2b54c29f126fedc75e349781f50dc09bb521a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 16:10:05 -0300 Subject: [PATCH 107/177] Fixed order delete --- includes/api/class-wc-rest-orders-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 634d33fc979..f0e6d2c33ff 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -92,7 +92,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'args' => array( 'force' => array( 'default' => false, - 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), 'reassign' => array(), ), From 6629395797a502d1e7fca541d566f35dda1b3b23 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 16:17:11 -0300 Subject: [PATCH 108/177] Fixed description for orders schema --- includes/api/class-wc-rest-orders-controller.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index f0e6d2c33ff..a0879089da8 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -1410,7 +1410,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'readonly' => true, ), 'line_items' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), + 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( @@ -1534,7 +1534,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), ), 'tax_lines' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), + 'description' => __( 'Tax lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'readonly' => true, @@ -1584,7 +1584,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), ), 'shipping_lines' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), + 'description' => __( 'Shipping lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( @@ -1638,7 +1638,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), ), 'fee_lines' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), + 'description' => __( 'Fee lines data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( @@ -1702,7 +1702,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ), ), 'coupon_lines' => array( - 'description' => __( 'Last order data.', 'woocommerce' ), + 'description' => __( 'Coupons line data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( From 0569ad3ca7ff60c13ed3be5326d7f34ad482cac8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 16:20:58 -0300 Subject: [PATCH 109/177] Refunds schema and improve delete and query --- ...class-wc-rest-order-refunds-controller.php | 289 +++++++++++++++++- 1 file changed, 287 insertions(+), 2 deletions(-) diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index 9894ce379b6..c3d0a15c51a 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -43,11 +43,47 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'shop_order_refund'; + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + /** * Register the routes for order refunds. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -55,10 +91,259 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { 'args' => array( 'force' => array( 'default' => false, - 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array(), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read orders. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list order refunds.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create orders. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete an order. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = 'any'; + + return $args; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Line itens data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line total tax.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), ), ), ), - ) ); + ); + + return $this->add_additional_fields_schema( $schema ); } } From 90bdabcf41a98d095dd17a0cfa778dfffbae3697 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 16:41:17 -0300 Subject: [PATCH 110/177] Added methods to list and get order refunds --- ...class-wc-rest-order-refunds-controller.php | 154 ++++++++++++++++++ .../api/class-wc-rest-orders-controller.php | 2 +- 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index c3d0a15c51a..3a1ccf13e4e 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -170,6 +170,160 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { return true; } + /** + * Prepare a single order refund output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + global $wpdb; + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + $refund = wc_get_order( $post ); + + if ( ! $refund || intval( $refund->post->post_parent ) !== intval( $order->id ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = ! empty( $request['dp'] ) ? intval( $request['dp'] ) : 2; + + $data = array( + 'id' => $refund->id, + 'date_created' => wc_rest_api_prepare_date_response( $refund->date ), + 'amount' => wc_format_decimal( $refund->get_refund_amount(), $dp ), + 'reason' => $refund->get_refund_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $refund->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['value'], + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + // if ( in_array( 'products', $expand ) ) { + // $_product_data = WC()->api->WC_API_Products->get_product( $product_id ); + + // if ( isset( $_product_data['product'] ) ) { + // $line_item['product_data'] = $_product_data['product']; + // } + // } + + $data['line_items'][] = $line_item; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $refund ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order_Refund $refund Comment object. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund ) { + $order_id = $refund->post->post_parent; + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + /** * Query args. * diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index a0879089da8..6f9e51796c6 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -172,7 +172,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } /** - * Prepare a single coupon output for response. + * Prepare a single order output for response. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. From 9b9d40d7e9d4801155d445c3da837c84210494b8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 28 Mar 2016 17:33:13 -0300 Subject: [PATCH 111/177] Create order refunds --- ...class-wc-rest-order-refunds-controller.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index 3a1ccf13e4e..b19b372f674 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -338,6 +338,82 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { return $args; } + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_data = get_post( (int) $request['order_id'] ); + + if ( empty( $order_data ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + $api_refund = is_bool( $request['api_refund'] ) ? $request['api_refund'] : true; + + $data = array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'line_items' => $request['line_items'], + ); + + // Create the refund. + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API. + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_data ); + + if ( isset( $payment_gateways[ $order->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->payment_method ]->process_refund( $order_id, $refund->get_refund_amount(), $refund->get_refund_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API', 'woocommerce' ), 500 ); + } + } + } + + $post = get_post( $refund->id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + /** * Get the Order's schema, conforming to JSON Schema. * From cb2b9b3e5071c7f752afd558560a27baffac782e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 15:48:08 -0300 Subject: [PATCH 112/177] Products schema --- .../api/class-wc-rest-products-controller.php | 830 ++++++++++++++++++ 1 file changed, 830 insertions(+) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 81b2ae51667..a7aae5f8655 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -47,7 +47,37 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { * Register the routes for products. */ public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -57,8 +87,808 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'default' => false, 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), ), + 'reassign' => array(), ), ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price_dates_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price_dates_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchaseable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_type' => array( + 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array( 'standard', 'application', 'music' ), + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'managing_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if a product is on backorder.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional Information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected term name of the attribute.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Variation ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price_dates_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price_dates_to' => array( + 'description' => __( 'End data of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchaseable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'File MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'managing_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if a product is on backorder.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), get_option( 'woocommerce_weight_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Varition image data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'slug' => array( + 'description' => __( 'Attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional Information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } } From 8219cee2564f7980b2b540ff0399a96cb451bf94 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 17:20:15 -0300 Subject: [PATCH 113/177] Added methods to list and get products --- .../api/class-wc-rest-orders-controller.php | 2 +- .../api/class-wc-rest-products-controller.php | 397 ++++++++++++++++++ 2 files changed, 398 insertions(+), 1 deletion(-) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 6f9e51796c6..b191b3d6233 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -429,7 +429,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { /** * Prepare links for the request. * - * @param WC_Order $order Comment object. + * @param WC_Order $order Order object. * @return array Links for the given order. */ protected function prepare_links( $order ) { diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index a7aae5f8655..f3cbf0f6640 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -43,6 +43,13 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'product'; + /** + * Initialize product actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + /** * Register the routes for products. */ @@ -94,6 +101,371 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { ) ); } + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = $request['status']; + + return $args; + } + + /** + * Get product data. + * + * @param WC_Product $product + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, + 'name' => $product->get_title(), + 'slug' => $product->get_post_data()->name, + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'type' => $product->product_type, + 'status' => $product->get_post_data()->post_status, + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->visibility, + 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'sale_price_dates_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', + 'sale_price_dates_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchaseable' => $product->is_purchasable(), + 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => (int) $product->download_limit, + 'download_expiry' => (int) $product->download_expiry, + 'download_type' => $product->download_type ? $product->download_type : 'standard', + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->backorders, + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => (int) $product->get_shipping_class_id(), + 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + '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() ), + 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), + 'categories' => array(), + 'tags' => array(), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => array(), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product + * @return array + */ + protected 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(), + 'date_created' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'sale_price_dates_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'sale_price_dates_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchaseable' => $variation->is_purchasable(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $variation->download_limit, + 'download_expiry' => (int) $variation->download_expiry, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->backorders, + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + ); + } + + return $variations; + } + + /** + * Get the downloads for a product or product variation + * + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_files() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $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, + 'date_created' => wc_rest_api_prepare_date_response( $attachment_post->post_date_gmt ), + 'date_modified' => wc_rest_api_prepare_date_response( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_api_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_api_prepare_date_response( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get the attributes for a product or product variation + * + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + // Variation attributes. + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), + 'slug' => str_replace( 'attribute_', '', 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' => wc_attribute_label( $attribute['name'] ), + 'slug' => str_replace( 'pa_', '', $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => array_map( 'trim', $options ), + ); + } + } + + return $attributes; + } + + /** + * Get product menu order. + * + * @param WC_Product $product + * @return int + */ + protected function get_product_menu_order( $product ) { + $menu_order = $product->get_post_data()->menu_order; + + if ( $product->is_type( 'variation' ) ) { + $variation = get_post( $product->get_variation_id() ); + $menu_order = $variation->menu_order; + } + + return apply_filters( 'woocommerce_rest_product_menu_order', $menu_order, $product ); + } + + /** + * Prepare a single product output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $product = wc_get_product( $post ); + + $data = $this->get_product_data( $product ); + + // Add variations to variable products. + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $data['variations'] = $this->get_variation_data( $product ); + } + + // Add grouped products data. + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $data['grouped_products'] = $product->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $product ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Product $product Product object. + * @return array Links for the given product. + */ + protected function prepare_links( $product ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $product->is_type( 'variation' ) && $product->parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->parent ) ), + ); + } else if ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->post->post_parent ) ), + ); + } + + return $links; + } + /** * Get the Order's schema, conforming to JSON Schema. * @@ -116,6 +488,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', @@ -891,4 +1268,24 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } } From d781e10d63dbd4fa9718bb23aa24ff34d578ee1f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 17:24:32 -0300 Subject: [PATCH 114/177] Coding standards --- includes/api/class-wc-rest-products-controller.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index f3cbf0f6640..109b9bbe91e 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -247,7 +247,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } /** - * Get the downloads for a product or product variation + * Get the downloads for a product or product variation. * * @param WC_Product|WC_Product_Variation $product * @return array @@ -287,11 +287,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { - // Add featured image + // Add featured image. if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } - // Add gallery images + // Add gallery images. $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() ); } @@ -318,7 +318,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { ); } - // Set a placeholder image if the product has no images set + // Set a placeholder image if the product has no images set. if ( empty( $images ) ) { $images[] = array( 'id' => 0, @@ -335,7 +335,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } /** - * Get the attributes for a product or product variation + * Get the attributes for a product or product variation. * * @param WC_Product|WC_Product_Variation $product * @return array @@ -457,7 +457,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->parent ) ), ); - } else if ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { + } elseif ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { $links['up'] = array( 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->post->post_parent ) ), ); @@ -467,7 +467,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } /** - * Get the Order's schema, conforming to JSON Schema. + * Get the Product's schema, conforming to JSON Schema. * * @return array */ From a0290a6c65bc8b6f75e3516a9b52e217a870af53 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 19:09:55 -0300 Subject: [PATCH 115/177] Stop check again if coupon have code when creating --- includes/api/class-wc-rest-coupons-controller.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 9d0fe37dab2..e0df8ab388b 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -177,13 +177,6 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { $schema = $this->get_item_schema(); - // Validate required POST fields. - if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { - if ( empty( $request['code'] ) ) { - return new WP_Error( 'woocommerce_rest_missing_parameter', sprintf( __( 'Missing parameter %s.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); - } - } - // Code. if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); From ae2b3eb3fce484e521f0d1c20aeb29a57cbf0ac0 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 19:11:49 -0300 Subject: [PATCH 116/177] Check if coupon code is empty --- includes/api/class-wc-rest-coupons-controller.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index e0df8ab388b..57afd3a707b 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -177,6 +177,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { $schema = $this->get_item_schema(); + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + // Code. if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); From 4a625fc2f2f27e6722474be9a8cbd0bdd63145c8 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 19:52:24 -0300 Subject: [PATCH 117/177] New rest functions file name --- includes/wc-core-functions.php | 2 +- includes/{wc-api-functions.php => wc-rest-functions.php} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename includes/{wc-api-functions.php => wc-rest-functions.php} (98%) diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php index 3927ab9eee0..0a32d707ef5 100644 --- a/includes/wc-core-functions.php +++ b/includes/wc-core-functions.php @@ -26,7 +26,7 @@ include( 'wc-product-functions.php' ); include( 'wc-account-functions.php' ); include( 'wc-term-functions.php' ); include( 'wc-attribute-functions.php' ); -include( 'wc-api-functions.php' ); +include( 'wc-rest-functions.php' ); /** * Filters on data used in admin and frontend. diff --git a/includes/wc-api-functions.php b/includes/wc-rest-functions.php similarity index 98% rename from includes/wc-api-functions.php rename to includes/wc-rest-functions.php index 7415a59ad51..79ec0c24729 100644 --- a/includes/wc-api-functions.php +++ b/includes/wc-rest-functions.php @@ -1,8 +1,8 @@ Date: Tue, 29 Mar 2016 19:54:37 -0300 Subject: [PATCH 118/177] Fixed rest functions names --- includes/api/class-wc-rest-coupons-controller.php | 6 +++--- includes/api/class-wc-rest-customers-controller.php | 6 +++--- includes/api/class-wc-rest-order-notes-controller.php | 2 +- includes/api/class-wc-rest-order-refunds-controller.php | 2 +- includes/api/class-wc-rest-orders-controller.php | 6 +++--- .../api/class-wc-rest-product-categories-controller.php | 4 ++-- includes/api/class-wc-rest-report-sales-controller.php | 4 ++-- includes/api/class-wc-rest-webhook-deliveries.php | 2 +- includes/api/class-wc-rest-webhooks-controller.php | 4 ++-- includes/wc-rest-functions.php | 8 ++++---- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 57afd3a707b..c42c7201c9c 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -116,8 +116,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, - 'created_at' => wc_rest_api_prepare_date_response( $post->post_date_gmt ), - 'updated_at' => wc_rest_api_prepare_date_response( $post->post_modified_gmt ), + 'created_at' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'updated_at' => wc_rest_prepare_date_response( $post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), @@ -126,7 +126,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, - 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_api_prepare_date_response( $coupon->expiry_date ) : null, + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_prepare_date_response( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index ef0779f2614..18cdc13c380 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -492,15 +492,15 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $data = array( 'id' => $customer->ID, - 'created_at' => wc_rest_api_prepare_date_response( $customer->user_registered ), - 'updated_at' => $customer->last_update ? wc_rest_api_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, + 'created_at' => wc_rest_prepare_date_response( $customer->user_registered ), + 'updated_at' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'last_order' => array( 'id' => is_object( $last_order ) ? $last_order->id : null, - 'date' => is_object( $last_order ) ? wc_rest_api_prepare_date_response( $last_order->post->post_date_gmt ) : null + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->post->post_date_gmt ) : null ), 'orders_count' => wc_get_customer_order_count( $customer->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index f38176cfa65..38c6aa652bc 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -307,7 +307,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => $note->comment_ID, - 'created_at' => wc_rest_api_prepare_date_response( $note->comment_date_gmt ), + 'created_at' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index b19b372f674..b55d58fea9a 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -196,7 +196,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { $data = array( 'id' => $refund->id, - 'date_created' => wc_rest_api_prepare_date_response( $refund->date ), + 'date_created' => wc_rest_prepare_date_response( $refund->date ), 'amount' => wc_format_decimal( $refund->get_refund_amount(), $dp ), 'reason' => $refund->get_refund_reason(), 'line_items' => array(), diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index b191b3d6233..b72c3546f3c 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -192,8 +192,8 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'currency' => $order->get_order_currency(), 'version' => $order->order_version, 'prices_include_tax' => $order->prices_include_tax, - 'date_created' => wc_rest_api_prepare_date_response( $post->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $post->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), 'customer_id' => $order->get_user_id(), 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), 'discount_tax' => wc_format_decimal( $order->cart_discount_tax, $dp ), @@ -211,7 +211,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'customer_user_agent' => $order->customer_user_agent, 'created_via' => $order->created_via, 'customer_note' => $order->customer_note, - 'date_completed' => wc_rest_api_prepare_date_response( $order->completed_date, true ), + 'date_completed' => wc_rest_prepare_date_response( $order->completed_date, true ), 'date_paid' => $order->paid_date, 'cart_hash' => $order->cart_hash, 'line_items' => array(), diff --git a/includes/api/class-wc-rest-product-categories-controller.php b/includes/api/class-wc-rest-product-categories-controller.php index b1a8073d35e..18b490bdce9 100644 --- a/includes/api/class-wc-rest-product-categories-controller.php +++ b/includes/api/class-wc-rest-product-categories-controller.php @@ -104,13 +104,13 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { update_woocommerce_term_meta( $id, 'display_type', $request['display'] ); if ( ! empty( $request['image'] ) ) { - $upload = wc_rest_api_upload_image_from_url( esc_url_raw( $request['image'] ) ); + $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image'] ) ); if ( is_wp_error( $upload ) ) { return $upload; } - $image_id = wc_rest_api_set_uploaded_image_as_attachment( $upload ); + $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); // Check if image_id is a valid image attachment before updating the term meta. if ( $image_id && wp_attachment_is_image( $image_id ) ) { diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php index 8e20274f34f..84ec69865ee 100644 --- a/includes/api/class-wc-rest-report-sales-controller.php +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -380,14 +380,14 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), 'type' => 'string', 'format' => 'date', - 'validate_callback' => 'rest_validate_reports_request_arg', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), 'date_max' => array( 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), 'type' => 'string', 'format' => 'date', - 'validate_callback' => 'rest_validate_reports_request_arg', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', 'sanitize_callback' => 'sanitize_text_field', ), ); diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index 5767bd04c51..a841a0a20d2 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -154,7 +154,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { $data = (array) $log; // Add timestamp. - $data['created_at'] = wc_rest_api_prepare_date_response( $log->comment->comment_date_gmt ); + $data['created_at'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); // Remove comment object. unset( $data['comment'] ); diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index 6f5930973fe..b464088d23c 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -467,8 +467,8 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { 'event' => $webhook->get_event(), 'hooks' => $webhook->get_hooks(), 'delivery_url' => $webhook->get_delivery_url(), - 'created_at' => wc_rest_api_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), - 'updated_at' => wc_rest_api_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + 'created_at' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'updated_at' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 79ec0c24729..f2665c48cd8 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @param string|null $date * @return string|null ISO8601/RFC3339 formatted datetime. */ -function wc_rest_api_prepare_date_response( $date_gmt, $date = null ) { +function wc_rest_prepare_date_response( $date_gmt, $date = null ) { // Check if mysql_to_rfc3339 exists first! if ( ! function_exists( 'mysql_to_rfc3339' ) ) { return null; @@ -52,7 +52,7 @@ function wc_rest_api_prepare_date_response( $date_gmt, $date = null ) { * @param string $image_url * @return array|WP_Error Attachment data or error message. */ -function wc_rest_api_upload_image_from_url( $image_url ) { +function wc_rest_upload_image_from_url( $image_url ) { $file_name = basename( current( explode( '?', $image_url ) ) ); $wp_filetype = wp_check_filetype( $file_name, null ); $parsed_url = @parse_url( $image_url ); @@ -117,7 +117,7 @@ function wc_rest_api_upload_image_from_url( $image_url ) { * @param int $id Post ID. Default to 0. * @return int Attachment ID */ -function wc_rest_api_set_uploaded_image_as_attachment( $upload, $id = 0 ) { +function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { $info = wp_check_filetype( $upload['file'] ); $title = ''; $content = ''; @@ -160,7 +160,7 @@ function wc_rest_api_set_uploaded_image_as_attachment( $upload, $id = 0 ) { * @param string $param * @return WP_Error|boolean */ -function rest_validate_reports_request_arg( $value, $request, $param ) { +function wc_rest_validate_reports_request_arg( $value, $request, $param ) { $attributes = $request->get_attributes(); if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { From 5bf8c808fc5279325df07ba50cd798b36f0d01df Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 21:22:10 -0300 Subject: [PATCH 119/177] Added methods to create and edit products --- .../abstract-wc-rest-posts-controller.php | 11 +- .../api/class-wc-rest-products-controller.php | 1110 ++++++++++++++++- 2 files changed, 1111 insertions(+), 10 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 4601d7af77e..997fe7f9418 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -235,7 +235,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $meta_fields = $this->add_post_meta_fields( $post, $request ); if ( is_wp_error( $meta_fields ) ) { // Remove post. - wp_delete_post( $post->ID, true ); + $this->delete_post( $post ); return $meta_fields; } @@ -269,6 +269,15 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return true; } + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + wp_delete_post( $post->ID, true ); + } + /** * Update a single post. * diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 109b9bbe91e..bf74c8f4fc7 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -48,6 +48,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); } /** @@ -125,10 +126,10 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $data = array( 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'name' => $product->get_title(), - 'slug' => $product->get_post_data()->name, + 'slug' => $product->get_post_data()->post_name, 'permalink' => $product->get_permalink(), - 'date_created' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_status, 'featured' => $product->is_featured(), @@ -208,8 +209,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $variations[] = array( 'id' => $variation->get_variation_id(), - 'date_created' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), @@ -309,8 +310,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $images[] = array( 'id' => (int) $attachment_id, - 'date_created' => wc_rest_api_prepare_date_response( $attachment_post->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $attachment_post->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $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 ), @@ -322,8 +323,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { if ( empty( $images ) ) { $images[] = array( 'id' => 0, - 'date_created' => wc_rest_api_prepare_date_response( time() ), // Default to now. - 'date_modified' => wc_rest_api_prepare_date_response( time() ), + 'date_created' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), @@ -466,6 +467,1097 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $links; } + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $data->post_title = wp_filter_post_kses( $request['name'] ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $data->post_content = wp_filter_post_kses( $request['description'] ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['short_description'] ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $data->post_status = get_post_status_object( $request['status'] ) ? $request['status'] : 'draft'; + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $data->post_name = $request['slug']; + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $data->menu_order = (int) $request['menu_order']; + } + + // Comment status. + if ( ! empty( $request['reviews_allowed'] ) ) { + $data->comment_status = $request['reviews_allowed'] ? 'open' : 'closed'; + } + + // Only when creating products. + if ( empty( $request['id'] ) ) { + // Post type. + $data->post_type = $this->post_type; + + // Ping status. + $data->ping_status = 'closed'; + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Get attribute taxonomy by slug. + * + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Save product images. + * + * @param WC_Product $product + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && $image['position'] == 0 ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + } + + set_post_thumbnail( $product->id, $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + update_post_meta( $product->id, '_product_image_gallery', implode( ',', $gallery ) ); + } + } else { + delete_post_thumbnail( $product->id ); + update_post_meta( $product->id, '_product_image_gallery', '' ); + } + } + + /** + * Save product shipping data. + * + * @param WC_Product $product + * @param array $data + */ + private function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + update_post_meta( $product->id, '_weight', '' ); + update_post_meta( $product->id, '_length', '' ); + update_post_meta( $product->id, '_width', '' ); + update_post_meta( $product->id, '_height', '' ); + } else { + if ( isset( $data['weight'] ) ) { + update_post_meta( $product->id, '_weight', '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Height. + if ( isset( $data['height'] ) ) { + update_post_meta( $product->id, '_height', '' === $data['height'] ? '' : wc_format_decimal( $data['height'] ) ); + } + + // Width. + if ( isset( $data['width'] ) ) { + update_post_meta( $product->id, '_width', '' === $data['width'] ? '' : wc_format_decimal($data['width'] ) ); + } + + // Length. + if ( isset( $data['length'] ) ) { + update_post_meta( $product->id, '_length', '' === $data['length'] ? '' : wc_format_decimal( $data['length'] ) ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + wp_set_object_terms( $product->id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + } + } + + /** + * Save downloadable files. + * + * @param WC_Product $product + * @param array $downloads + * @param int $variation_id + */ + private function save_downloadable_files( $product, $downloads, $variation_id = 0 ) { + $files = array(); + + // File paths will be stored in an array keyed off md5(file path). + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( ! isset( $file['file'] ) ) { + continue; + } + + $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; + + if ( 0 === strpos( $file['file'], 'http' ) ) { + $file_url = esc_url_raw( $file['file'] ); + } else { + $file_url = wc_clean( $file['file'] ); + } + + $files[ md5( $file_url ) ] = array( + 'name' => $file_name, + 'file' => $file_url + ); + } + + // Grant permission to any newly added files on any existing orders for this product prior to saving. + do_action( 'woocommerce_process_product_file_download_paths', $product->id, $variation_id, $files ); + + $id = ( 0 === $variation_id ) ? $product->id : $variation_id; + + update_post_meta( $id, '_downloadable_files', $files ); + } + + /** + * Save product meta. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + global $wpdb; + + // Product Type. + $product_type = null; + if ( isset( $request['type'] ) ) { + $product_type = wc_clean( $request['type'] ); + wp_set_object_terms( $product->id, $product_type, 'product_type' ); + } else { + $_product_type = get_the_terms( $product->id, 'product_type' ); + if ( is_array( $_product_type ) ) { + $_product_type = current( $_product_type ); + $product_type = $_product_type->slug; + } + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + update_post_meta( $product->id, '_virtual', true === $request['virtual'] ? 'yes' : 'no' ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + update_post_meta( $product->id, '_tax_status', wc_clean( $request['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + update_post_meta( $product->id, '_tax_class', wc_clean( $request['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + update_post_meta( $product->id, '_visibility', wc_clean( $request['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + update_post_meta( $product->id, '_purchase_note', wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + update_post_meta( $product->id, '_featured', true === $request['featured'] ? 'yes' : 'no' ); + } + + // Shipping data. + $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $sku = get_post_meta( $product->id, '_sku', true ); + $new_sku = wc_clean( $request['sku'] ); + + if ( '' == $new_sku ) { + update_post_meta( $product->id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + update_post_meta( $product->id, '_sku', $new_sku ); + } + } else { + update_post_meta( $product->id, '_sku', '' ); + } + } + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms. + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->id, $values, $taxonomy ); + } + + if ( $values ) { + // Add attribute to array, but don't set values. + $attributes[ $taxonomy ] = array( + 'name' => $taxonomy, + 'value' => '', + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); + + // Text based, separate by pipe. + } else { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attributes[ $attribute_slug ] = array( + 'name' => wc_clean( $attribute['name'] ), + 'value' => $values, + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + } + + if ( ! function_exists( 'attributes_cmp' ) ) { + function attributes_cmp( $a, $b ) { + if ( $a['position'] == $b['position'] ) { + return 0; + } + + return ( $a['position'] < $b['position'] ) ? -1 : 1; + } + } + uasort( $attributes, 'attributes_cmp' ); + + update_post_meta( $product->id, '_product_attributes', $attributes ); + } + + // Sales and prices. + if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + update_post_meta( $product->id, '_regular_price', '' ); + update_post_meta( $product->id, '_sale_price', '' ); + update_post_meta( $product->id, '_sale_price_dates_from', '' ); + update_post_meta( $product->id, '_sale_price_dates_to', '' ); + update_post_meta( $product->id, '_price', '' ); + + } else { + + // Regular Price + if ( isset( $request['regular_price'] ) ) { + $regular_price = ( '' === $request['regular_price'] ) ? '' : $request['regular_price']; + } else { + $regular_price = get_post_meta( $product->id, '_regular_price', true ); + } + + // Sale Price + if ( isset( $request['sale_price'] ) ) { + $sale_price = ( '' === $request['sale_price'] ) ? '' : $request['sale_price']; + } else { + $sale_price = get_post_meta( $product->id, '_sale_price', true ); + } + + if ( isset( $request['sale_price_dates_from'] ) ) { + $date_from = $request['sale_price_dates_from']; + } else { + $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $request['sale_price_dates_to'] ) ) { + $date_to = $request['sale_price_dates_to']; + } else { + $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $product->id, $regular_price, $sale_price, $date_from, $date_to ); + } + + // Product parent ID for groups. + $parent_id = 0; + if ( isset( $request['parent_id'] ) ) { + $parent_id = wp_update_post( array( 'ID' => $product->id, 'post_parent' => absint( $request['parent_id'] ) ) ); + } + + // Update parent if grouped so price sorting works and stays in sync with the cheapest child. + if ( $parent_id > 0 || $product_type == 'grouped' ) { + + $clear_parent_ids = array(); + + if ( $parent_id > 0 ) { + $clear_parent_ids[] = $parent_id; + } + + if ( $product_type == 'grouped' ) { + $clear_parent_ids[] = $product->id; + } + + if ( $clear_parent_ids ) { + foreach ( $clear_parent_ids as $clear_id ) { + + $children_by_price = get_posts( array( + 'post_parent' => $clear_id, + 'orderby' => 'meta_value_num', + 'order' => 'asc', + 'meta_key' => '_price', + 'posts_per_page' => 1, + 'post_type' => 'product', + 'fields' => 'ids' + ) ); + + if ( $children_by_price ) { + foreach ( $children_by_price as $child ) { + $child_price = get_post_meta( $child, '_price', true ); + update_post_meta( $clear_id, '_price', $child_price ); + } + } + } + } + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + update_post_meta( $product->id, '_sold_individually', true === $request['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $product->id, '_stock_status', true ); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['managing_stock'] ) ) { + $managing_stock = ( true === $request['managing_stock'] ) ? 'yes' : 'no'; + update_post_meta( $product->id, '_manage_stock', $managing_stock ); + } else { + $managing_stock = get_post_meta( $product->id, '_manage_stock', true ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + if ( 'notify' === $request['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $request['backorders'] ) ? 'yes' : 'no'; + } + + update_post_meta( $product->id, '_backorders', $backorders ); + } else { + $backorders = get_post_meta( $product->id, '_backorders', true ); + } + + if ( 'grouped' == $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } elseif ( 'external' == $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + update_post_meta( $product->id, '_backorders', $backorders ); + + wc_update_product_stock_status( $product->id, $stock_status ); + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + wc_update_product_stock( $product->id, wc_stock_amount( $request['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $product->id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $product->id, wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', $backorders ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } + + } else { + wc_update_product_stock_status( $product->id, $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + update_post_meta( $product->id, '_upsell_ids', $upsells ); + } else { + delete_post_meta( $product->id, '_upsell_ids' ); + } + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + update_post_meta( $product->id, '_crosssell_ids', $crosssells ); + } else { + delete_post_meta( $product->id, '_crosssell_ids' ); + } + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $term_ids = array_unique( array_map( 'intval', $request['categories'] ) ); + wp_set_object_terms( $product->id, $term_ids, 'product_cat' ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $term_ids = array_unique( array_map( 'intval', $request['tags'] ) ); + wp_set_object_terms( $product->id, $term_ids, 'product_tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $is_downloadable = true === $request['downloadable'] ? 'yes' : 'no'; + update_post_meta( $product->id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $product->id, '_downloadable', true ); + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + update_post_meta( $product->id, '_download_limit', ( '' === $request['download_limit'] ) ? '' : absint( $request['download_limit'] ) ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + update_post_meta( $product->id, '_download_expiry', ( '' === $request['download_expiry'] ) ? '' : absint( $request['download_expiry'] ) ); + } + + // Download type. + if ( isset( $request['download_type'] ) ) { + update_post_meta( $product->id, '_download_type', wc_clean( $request['download_type'] ) ); + } + } + + // Product url and button text for external products. + if ( $product_type == 'external' ) { + if ( isset( $request['external_url'] ) ) { + update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); + } + + if ( isset( $request['button_text'] ) ) { + update_post_meta( $product->id, '_button_text', wc_clean( $request['button_text'] ) ); + } + } + + return true; + } + + /** + * Save variations. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $variations = $request['variations']; + $attributes = (array) maybe_unserialize( get_post_meta( $product->id, '_product_attributes', true ) ); + + foreach ( $variations as $menu_order => $variation ) { + $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; + + // Generate a useful post title. + $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $product->id ) ) ); + + // Update or Add post. + if ( ! $variation_id ) { + $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; + + $new_variation = array( + 'post_title' => $variation_post_title, + 'post_content' => '', + 'post_status' => $post_status, + 'post_author' => get_current_user_id(), + 'post_parent' => $product->id, + 'post_type' => 'product_variation', + 'menu_order' => $menu_order + ); + + $variation_id = wp_insert_post( $new_variation ); + + do_action( 'woocommerce_create_product_variation', $variation_id ); + } else { + $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); + if ( isset( $variation['visible'] ) ) { + $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; + $update_variation['post_status'] = $post_status; + } + + $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); + + do_action( 'woocommerce_update_product_variation', $variation_id ); + } + + // Stop with we don't have a variation ID. + if ( is_wp_error( $variation_id ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); + } + + // SKU. + if ( isset( $variation['sku'] ) ) { + $sku = get_post_meta( $variation_id, '_sku', true ); + $new_sku = wc_clean( $variation['sku'] ); + + if ( '' == $new_sku ) { + update_post_meta( $variation_id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 ); + } else { + update_post_meta( $variation_id, '_sku', $new_sku ); + } + } else { + update_post_meta( $variation_id, '_sku', '' ); + } + } + } + + // Thumbnail. + if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { + $image = current( $variation['image'] ); + if ( $image && is_array( $image ) ) { + if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { + $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + + update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); + } + } else { + delete_post_meta( $variation_id, '_thumbnail_id' ); + } + } + + // Virtual variation. + if ( isset( $variation['virtual'] ) ) { + $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_virtual', $is_virtual ); + } + + // Downloadable variation. + if ( isset( $variation['downloadable'] ) ) { + $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); + } + + // Shipping data. + $this->save_product_shipping_data( $variation_id, $variation ); + + // Stock handling. + if ( isset( $variation['managing_stock'] ) ) { + $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_manage_stock', $managing_stock ); + } else { + $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); + } + + // Only update stock status to user setting if changed by the user, + // but do so before looking at stock levels at variation level. + if ( isset( $variation['in_stock'] ) ) { + $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; + wc_update_product_stock_status( $variation_id, $stock_status ); + } + + if ( 'yes' === $managing_stock ) { + $backorders = get_post_meta( $variation_id, '_backorders', true ); + + if ( isset( $variation['backorders'] ) ) { + if ( 'notify' == $variation['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; + } + } + + update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); + + if ( isset( $variation['stock_quantity'] ) ) { + wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); + } + } else { + delete_post_meta( $variation_id, '_backorders' ); + delete_post_meta( $variation_id, '_stock' ); + } + + // Regular Price. + if ( isset( $variation['regular_price'] ) ) { + $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; + } else { + $regular_price = get_post_meta( $variation_id, '_regular_price', true ); + } + + // Sale Price. + if ( isset( $variation['sale_price'] ) ) { + $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; + } else { + $sale_price = get_post_meta( $variation_id, '_sale_price', true ); + } + + if ( isset( $variation['sale_price_dates_from'] ) ) { + $date_from = $variation['sale_price_dates_from']; + } else { + $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $variation['sale_price_dates_to'] ) ) { + $date_to = $variation['sale_price_dates_to']; + } else { + $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); + + // Tax class. + if ( isset( $variation['tax_class'] ) ) { + if ( $variation['tax_class'] !== 'parent' ) { + update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); + } else { + delete_post_meta( $variation_id, '_tax_class' ); + } + } + + // Downloads. + if ( 'yes' == $is_downloadable ) { + // Downloadable files. + if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { + $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); + } + + // Download limit. + if ( isset( $variation['download_limit'] ) ) { + $download_limit = absint( $variation['download_limit'] ); + update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); + } + + // Download expiry. + if ( isset( $variation['download_expiry'] ) ) { + $download_expiry = absint( $variation['download_expiry'] ); + update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); + } + } else { + update_post_meta( $variation_id, '_download_limit', '' ); + update_post_meta( $variation_id, '_download_expiry', '' ); + update_post_meta( $variation_id, '_downloadable_files', '' ); + } + + // Description. + if ( isset( $variation['description'] ) ) { + update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); + } + + // Update taxonomies. + if ( isset( $variation['attributes'] ) ) { + $updated_attribute_keys = array(); + + foreach ( $variation['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); + $updated_attribute_keys[] = $_attribute_key; + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + update_post_meta( $variation_id, $_attribute_key, $_attribute_value ); + } + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $variation_id, $key ); + } + } + + do_action( 'woocommerce_rest_save_product_variation', $variation_id, $menu_order, $variation ); + } + + // Update parent if variable so price sorting works and stays in sync with the cheapest child. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + update_post_meta( $product->id, '_default_attributes', $default_attributes ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( isset( $request['type'] ) && 'variable' == $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations( $product, $request ); + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations( $product, $request ); + } else { + // Just sync variations. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + } + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + // Delete product attachments. + $attachments = get_children( array( + 'post_parent' => $post->ID, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + wp_delete_post( $post->ID, true ); + } + /** * Get the Product's schema, conforming to JSON Schema. * From f675d49a54bc2f8680e4a0efd28d37ec45ad988f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 21:46:35 -0300 Subject: [PATCH 120/177] Return product default attributes --- .../api/class-wc-rest-products-controller.php | 286 ++++++++++-------- 1 file changed, 154 insertions(+), 132 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index bf74c8f4fc7..c99ca97bdda 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -116,137 +116,6 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $args; } - /** - * Get product data. - * - * @param WC_Product $product - * @return array - */ - protected function get_product_data( $product ) { - $data = array( - 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, - 'name' => $product->get_title(), - 'slug' => $product->get_post_data()->post_name, - 'permalink' => $product->get_permalink(), - 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), - 'type' => $product->product_type, - 'status' => $product->get_post_data()->post_status, - 'featured' => $product->is_featured(), - 'catalog_visibility' => $product->visibility, - 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), - 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), - 'sku' => $product->get_sku(), - 'price' => $product->get_price(), - 'regular_price' => $product->get_regular_price(), - 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', - 'sale_price_dates_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', - 'sale_price_dates_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', - 'price_html' => $product->get_price_html(), - 'on_sale' => $product->is_on_sale(), - 'purchaseable' => $product->is_purchasable(), - 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), - 'virtual' => $product->is_virtual(), - 'downloadable' => $product->is_downloadable(), - 'downloads' => $this->get_downloads( $product ), - 'download_limit' => (int) $product->download_limit, - 'download_expiry' => (int) $product->download_expiry, - 'download_type' => $product->download_type ? $product->download_type : 'standard', - 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', - 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', - 'tax_status' => $product->get_tax_status(), - 'tax_class' => $product->get_tax_class(), - 'managing_stock' => $product->managing_stock(), - 'stock_quantity' => $product->get_stock_quantity(), - 'in_stock' => $product->is_in_stock(), - 'backorders' => $product->backorders, - 'backorders_allowed' => $product->backorders_allowed(), - 'backordered' => $product->is_on_backorder(), - 'sold_individually' => $product->is_sold_individually(), - 'weight' => $product->get_weight(), - 'length' => $product->get_length(), - 'width' => $product->get_width(), - 'height' => $product->get_height(), - 'shipping_required' => $product->needs_shipping(), - 'shipping_taxable' => $product->is_shipping_taxable(), - 'shipping_class' => $product->get_shipping_class(), - 'shipping_class_id' => (int) $product->get_shipping_class_id(), - 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), - 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), - '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() ), - 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, - 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), - 'categories' => array(), - 'tags' => array(), - 'images' => $this->get_images( $product ), - 'attributes' => $this->get_attributes( $product ), - 'default_attributes' => array(), - 'variations' => array(), - 'grouped_products' => array(), - 'menu_order' => $this->get_product_menu_order( $product ), - ); - - return $data; - } - - /** - * Get an individual variation's data. - * - * @param WC_Product $product - * @return array - */ - protected 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(), - 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price(), - 'sale_price_dates_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', - 'sale_price_dates_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', - 'on_sale' => $variation->is_on_sale(), - 'purchaseable' => $variation->is_purchasable(), - 'virtual' => $variation->is_virtual(), - 'downloadable' => $variation->is_downloadable(), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => (int) $variation->download_limit, - 'download_expiry' => (int) $variation->download_expiry, - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'managing_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders' => $variation->backorders, - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'weight' => $variation->get_weight(), - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => $variation->get_shipping_class_id(), - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), - ); - } - - return $variations; - } - /** * Get the downloads for a product or product variation. * @@ -391,7 +260,160 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $menu_order = $variation->menu_order; } - return apply_filters( 'woocommerce_rest_product_menu_order', $menu_order, $product ); + return $menu_order; + } + + /** + * Get default attributes. + * + * @param WC_Product $product + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( (array) get_post_meta( $product->id, '_default_attributes', true ) as $key => $value ) { + $default[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $key ) ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ), + 'option' => $value, + ); + } + } + + return $default; + } + + /** + * Get product data. + * + * @param WC_Product $product + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, + 'name' => $product->get_title(), + 'slug' => $product->get_post_data()->post_name, + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'type' => $product->product_type, + 'status' => $product->get_post_data()->post_status, + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->visibility, + 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'sale_price_dates_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', + 'sale_price_dates_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchaseable' => $product->is_purchasable(), + 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => (int) $product->download_limit, + 'download_expiry' => (int) $product->download_expiry, + 'download_type' => $product->download_type ? $product->download_type : 'standard', + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->backorders, + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => (int) $product->get_shipping_class_id(), + 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + '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() ), + 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), + 'categories' => array(), + 'tags' => array(), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product + * @return array + */ + protected 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(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'sale_price_dates_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'sale_price_dates_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchaseable' => $variation->is_purchasable(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $variation->download_limit, + 'download_expiry' => (int) $variation->download_expiry, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->backorders, + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + ); + } + + return $variations; } /** From dc6b4e95c8f9d41834e12efe3f07553891721572 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 21:57:05 -0300 Subject: [PATCH 121/177] Added method to show product taxonomy terms --- .../api/class-wc-rest-products-controller.php | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index c99ca97bdda..bdb8d723d8e 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -138,6 +138,27 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $downloads; } + /** + * Get taxonomy terms. + * + * @param WC_Product $product + * @param string $taxonomy + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wp_get_post_terms( $product->id, 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + /** * Get the images for a product or product variation. * @@ -204,6 +225,28 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $images; } + /** + * Get default attributes. + * + * @param WC_Product $product + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( (array) get_post_meta( $product->id, '_default_attributes', true ) as $key => $value ) { + $default[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $key ) ), + 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ), + 'option' => $value, + ); + } + } + + return $default; + } + /** * Get the attributes for a product or product variation. * @@ -263,28 +306,6 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $menu_order; } - /** - * Get default attributes. - * - * @param WC_Product $product - * @return array - */ - protected function get_default_attributes( $product ) { - $default = array(); - - if ( $product->is_type( 'variable' ) ) { - foreach ( (array) get_post_meta( $product->id, '_default_attributes', true ) as $key => $value ) { - $default[] = array( - 'name' => wc_attribute_label( str_replace( 'attribute_', '', $key ) ), - 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ), - 'option' => $value, - ); - } - } - - return $default; - } - /** * Get product data. * @@ -348,8 +369,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), - 'categories' => array(), - 'tags' => array(), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), 'images' => $this->get_images( $product ), 'attributes' => $this->get_attributes( $product ), 'default_attributes' => $this->get_default_attributes( $product ), From 124a4291b0c455553d979d5099512abcbeb99da0 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 29 Mar 2016 22:06:05 -0300 Subject: [PATCH 122/177] Created new method to save product taxonomy terms --- .../api/class-wc-rest-products-controller.php | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index bdb8d723d8e..e5835b9f053 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -746,6 +746,23 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { update_post_meta( $id, '_downloadable_files', $files ); } + /** + * Save taxonomy terms. + * + * @param WC_Product $product + * @param array $terms + * @param string $taxonomy + * @return array + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + $term_ids = array_unique( array_map( 'intval', $term_ids ) ); + + wp_set_object_terms( $product->id, $term_ids, 'product_' . $taxonomy ); + + return $terms; + } + /** * Save product meta. * @@ -1117,14 +1134,12 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Product categories. if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { - $term_ids = array_unique( array_map( 'intval', $request['categories'] ) ); - wp_set_object_terms( $product->id, $term_ids, 'product_cat' ); + $this->save_taxonomy_terms( $product, $request['categories'] ); } // Product tags. if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { - $term_ids = array_unique( array_map( 'intval', $request['tags'] ) ); - wp_set_object_terms( $product->id, $term_ids, 'product_tag' ); + $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); } // Downloadable. @@ -1181,7 +1196,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { * @return bool * @throws WC_REST_Exception */ - protected function save_variations( $product, $request ) { + protected function save_variations_data( $product, $request ) { global $wpdb; $variations = $request['variations']; @@ -1526,7 +1541,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Save variations. if ( isset( $request['type'] ) && 'variable' == $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { - $this->save_variations( $product, $request ); + $this->save_variations_data( $product, $request ); } return true; @@ -1557,7 +1572,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Save variations. if ( $product->is_type( 'variable' ) ) { if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { - $this->save_variations( $product, $request ); + $this->save_variations_data( $product, $request ); } else { // Just sync variations. WC_Product_Variable::sync( $product->id ); From f4012b7f1f0e0d1937d89946f6743a092d2fe3eb Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 11:17:16 -0300 Subject: [PATCH 123/177] Reviewed post permissions --- .../abstract-wc-rest-posts-controller.php | 79 +++++-------------- .../class-wc-rest-order-notes-controller.php | 2 +- ...class-wc-rest-order-refunds-controller.php | 70 ---------------- .../api/class-wc-rest-orders-controller.php | 70 ---------------- .../api/class-wc-rest-webhooks-controller.php | 70 ---------------- includes/wc-rest-functions.php | 28 +++++++ 6 files changed, 49 insertions(+), 270 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 997fe7f9418..6bd272e98ef 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -43,19 +43,15 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { protected $public = false; /** - * Check if a given request has access to read an item. + * Check if a given request has access to read items. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ - public function get_item_permissions_check( $request ) { - $post = get_post( (int) $request['id'] ); - - if ( $post ) { - return $this->check_read_permission( $post ); + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } - - return true; } /** @@ -65,26 +61,27 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { - - $post_type = get_post_type_object( $this->post_type ); - - if ( ! current_user_can( $post_type->cap->create_posts ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', sprintf( __( 'Sorry, you are not allowed to create a new %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** - * Check if a given request has access to read items. + * Check if a given request has access to read an item. * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean */ - public function get_items_permissions_check( $request ) { - $post_type = get_post_type_object( $this->post_type ); + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); - return current_user_can( $post_type->cap->read_private_posts ); + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; } /** @@ -95,10 +92,9 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { */ public function update_item_permissions_check( $request ) { $post = get_post( $request['id'] ); - $post_type = get_post_type_object( $this->post_type ); - if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', sprintf( __( 'Sorry, you are not allowed to update this %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );; + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -113,48 +109,13 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { public function delete_item_permissions_check( $request ) { $post = get_post( $request['id'] ); - if ( $post && ! $this->check_delete_permission( $post ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } - /** - * Check if we can read an item. - * - * Correctly handles posts with the inherit status. - * - * @param object $post Post object. - * @return boolean Can we read it? - */ - public function check_read_permission( $post ) { - $post_type = get_post_type_object( $this->post_type ); - return 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); - } - - /** - * Check if we can edit an item. - * - * @param object $post Post object. - * @return boolean Can we edit it? - */ - protected function check_update_permission( $post ) { - $post_type = get_post_type_object( $this->post_type ); - return current_user_can( $post_type->cap->edit_post, $post->ID ); - } - - /** - * Check if we can delete an item. - * - * @param object $post Post object. - * @return boolean Can we delete it? - */ - protected function check_delete_permission( $post ) { - $post_type = get_post_type_object( $this->post_type ); - return current_user_can( $post_type->cap->delete_post, $post->ID ); - } - /** * Get a single item. * @@ -405,7 +366,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $posts = array(); foreach ( $query_result as $post ) { - if ( ! $this->check_read_permission( $post ) ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { continue; } @@ -483,7 +444,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { */ $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); - if ( ! $this->check_delete_permission( $post ) ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 38c6aa652bc..9fccd995649 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Order Notes controller class. * * @package WooCommerce/API - * @extends WC_REST_Posts_Controller + * @extends WP_REST_Controller */ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index b55d58fea9a..3f772b6fbaf 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -100,76 +100,6 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { ) ); } - /** - * Check whether a given request has permission to read orders. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list order refunds.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access create orders. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access update an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access delete an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - /** * Prepare a single order refund output for response. * diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index b72c3546f3c..2c18a7ca141 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -101,76 +101,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { ) ); } - /** - * Check whether a given request has permission to read orders. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list orders.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access create orders. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'publish_shop_orders' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access update an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access delete an order. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - /** * Prepare a single order output for response. * diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index b464088d23c..4cc70661ea4 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -110,76 +110,6 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { ) ); } - /** - * Check whether a given request has permission to read webhooks. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list webhooks.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access create webhooks. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access to read a webhook. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access update a webhook. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Check if a given request has access delete a webhook. - * - * @param WP_REST_Request $request Full details about the request. - * @return boolean - */ - public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - /** * Create a single webhook. * diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index f2665c48cd8..06244f885a7 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -199,3 +199,31 @@ function wc_rest_urlencode_rfc3986( $value ) { return str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) ); } } + +/** + * Check permissions of posts on REST API. + * + * @since 2.6.0 + * @param string $post_type Post type. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'read_private_posts', + 'create' => 'publish_posts', + 'edit' => 'edit_post', + 'delete' => 'delete_post', + ); + + if ( 'revision' === $post_type ) { + $permission = false; + } else { + $cap = $contexts[ $context ]; + $post_type_object = get_post_type_object( $post_type ); + $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_post_permissions', $permission, $post_type, $context, $object_id ); +} From 7afcc8482e4245e932abcf5a4bd92a8811aa1001 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 11:43:01 -0300 Subject: [PATCH 124/177] Improved permissions for order notes and webhook deliveries --- .../api/class-wc-rest-order-notes-controller.php | 14 +++++++++----- includes/api/class-wc-rest-webhook-deliveries.php | 8 +++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 9fccd995649..7566946d4f4 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -91,8 +91,8 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list order notes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_post_permissions( 'shop_order', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -105,7 +105,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'publish_shop_orders' ) ) { + if ( ! wc_rest_check_post_permissions( 'shop_order', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -119,7 +119,9 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + $post = get_post( (int) $request['order_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_order', 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -133,7 +135,9 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * @return boolean */ public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + $post = get_post( (int) $request['order_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_order', 'delete', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index a841a0a20d2..94a143b52cd 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -70,8 +70,8 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list taxes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_post_permissions( 'shop_webhook', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -84,7 +84,9 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + $post = get_post( (int) $request['webhook_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_webhook', 'read', $post->ID ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } From dedfe41c37299775b452d4513391a08bfe42f220 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 11:53:14 -0300 Subject: [PATCH 125/177] User permissions --- .../class-wc-rest-customers-controller.php | 24 +++++-------------- includes/wc-rest-functions.php | 21 ++++++++++++++++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index 18cdc13c380..7bfd4fc70bd 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -113,7 +113,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'list_users' ) ) { + if ( ! wc_rest_check_user_permissions( 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list customers.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -127,7 +127,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'create_users' ) ) { + if ( ! wc_rest_check_user_permissions( 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -141,21 +141,9 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - $id = (int) $request['id']; - $customer = get_userdata( $id ); - $types = get_post_types( array( 'public' => true ), 'names' ); + $id = (int) $request['id']; - if ( empty( $id ) || empty( $customer->ID ) ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - if ( get_current_user_id() === $id ) { - return true; - } - - if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); - } else if ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) { + if ( $post && ! wc_rest_check_user_permissions( 'read', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -171,7 +159,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { public function update_item_permissions_check( $request ) { $id = (int) $request['id']; - if ( ! current_user_can( 'edit_user', $id ) ) { + if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -187,7 +175,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { public function delete_item_permissions_check( $request ) { $id = (int) $request['id']; - if ( ! current_user_can( 'delete_user', $id ) ) { + if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 06244f885a7..4a4947cbf68 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -227,3 +227,24 @@ function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_ return apply_filters( 'woocommerce_rest_check_post_permissions', $permission, $post_type, $context, $object_id ); } + +/** + * Check permissions of users on REST API. + * + * @since 2.6.0 + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'list_users', + 'create' => 'edit_users', + 'edit' => 'edit_users', + 'delete' => 'delete_users', + ); + + $permission = current_user_can( $contexts[ $context ], $object_id ); + + return apply_filters( 'woocommerce_rest_check_user_permissions', $permission, $context, $object_id ); +} From 768492c02c7b32a9018dcdfbdc3fdbefac45061a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 11:54:49 -0300 Subject: [PATCH 126/177] Fixed customer permissions --- includes/api/class-wc-rest-customers-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index 7bfd4fc70bd..7a6529a2617 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -114,7 +114,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'read' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list customers.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -143,7 +143,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { public function get_item_permissions_check( $request ) { $id = (int) $request['id']; - if ( $post && ! wc_rest_check_user_permissions( 'read', $id ) ) { + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } From 0fce9c52438649d405eb1388541cc10fea1f0dec Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:17:40 -0300 Subject: [PATCH 127/177] New product terms permissions --- .../abstract-wc-rest-posts-controller.php | 2 + .../abstract-wc-rest-terms-controller.php | 59 +++++++++++-------- ...st-product-shipping-classes-controller.php | 25 -------- includes/wc-rest-functions.php | 28 ++++++++- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 6bd272e98ef..0d84ce0d9e4 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -52,6 +52,8 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } + + return true; } /** diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 3541dfff3d8..254e83358f8 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -91,12 +91,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { public function get_items_permissions_check( $request ) { $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( 'edit' === $request['context'] && ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { - return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_product_term_permissions( $taxonomy, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -111,11 +110,10 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { public function create_item_permissions_check( $request ) { $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) { + if ( ! wc_rest_check_product_term_permissions( $taxonomy, 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -129,7 +127,17 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - return $this->get_items_permissions_check( $request ); + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $term = get_term( (int) $request['id'], $taxonomy ); + if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'read', $term->term_id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; } /** @@ -141,16 +149,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { public function update_item_permissions_check( $request ) { $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } $term = get_term( (int) $request['id'], $taxonomy ); - if ( ! $term ) { - return new WP_Error( "woocommerce_rest_{$taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'edit', $term->term_id ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -166,16 +169,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { public function delete_item_permissions_check( $request ) { $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy ) { - return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } $term = get_term( (int) $request['id'], $taxonomy ); - if ( ! $term ) { - return new WP_Error( "woocommerce_rest_{$taxonomy}_term_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) { + if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'delete', $term->term_id ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -403,8 +401,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { */ public function update_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); - $schema = $this->get_item_schema(); + $term = get_term( (int) $request['id'], $taxonomy ); + if ( ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $schema = $this->get_item_schema(); $prepared_args = array(); if ( isset( $request['name'] ) ) { $prepared_args['name'] = $request['name']; @@ -430,8 +433,6 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $prepared_args['parent'] = $parent->term_id; } - $term = get_term( (int) $request['id'], $taxonomy ); - // Only update the term if we haz something to update. if ( ! empty( $prepared_args ) ) { $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); @@ -472,7 +473,13 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { */ public function delete_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + $term = get_term( (int) $request['id'], $taxonomy ); + + if ( ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { diff --git a/includes/api/class-wc-rest-product-shipping-classes-controller.php b/includes/api/class-wc-rest-product-shipping-classes-controller.php index 3d0070e5c2a..008cf0df7a2 100644 --- a/includes/api/class-wc-rest-product-shipping-classes-controller.php +++ b/includes/api/class-wc-rest-product-shipping-classes-controller.php @@ -43,31 +43,6 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll */ protected $taxonomy = 'product_shipping_class'; - /** - * Check if a given request has access to read the terms. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_items_permissions_check( $request ) { - $taxonomy = get_taxonomy( $this->taxonomy ); - - return current_user_can( $taxonomy->cap->edit_terms ); - } - - /** - * Check if a given request has access to read a term. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|boolean - */ - public function get_item_permissions_check( $request ) { - $id = (int) $request['id']; - $taxonomy = get_taxonomy( $this->taxonomy ); - - return current_user_can( $taxonomy->cap->edit_terms, $id ); - } - /** * Prepare a single product shipping class output for response. * diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 4a4947cbf68..564b21c3f7a 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -225,7 +225,7 @@ function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_ $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); } - return apply_filters( 'woocommerce_rest_check_post_permissions', $permission, $post_type, $context, $object_id ); + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type ); } /** @@ -246,5 +246,29 @@ function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { $permission = current_user_can( $contexts[ $context ], $object_id ); - return apply_filters( 'woocommerce_rest_check_user_permissions', $permission, $context, $object_id ); + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' ); +} + +/** + * Check permissions of product terms on REST API. + * + * @since 2.6.0 + * @param string $taxonomy Taxonomy. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'manage_terms', + 'create' => 'edit_terms', + 'edit' => 'edit_terms', + 'delete' => 'delete_terms', + ); + + $cap = $contexts[ $context ]; + $taxonomy_object = get_taxonomy( $taxonomy ); + $permission = current_user_can( $taxonomy_object->cap->$cap, $object_id ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); } From 82a6a5f18ea60f53f9675efea4db7f1ec6a15641 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:33:33 -0300 Subject: [PATCH 128/177] Settings and reports permissions --- .../abstract-wc-rest-posts-controller.php | 34 ------------------- .../class-wc-rest-report-sales-controller.php | 4 +-- .../api/class-wc-rest-reports-controller.php | 4 +-- .../class-wc-rest-tax-classes-controller.php | 6 ++-- .../api/class-wc-rest-taxes-controller.php | 10 +++--- includes/wc-rest-functions.php | 20 +++++++++++ 6 files changed, 32 insertions(+), 46 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 0d84ce0d9e4..2ec2904c374 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -174,23 +174,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $schema = $this->get_item_schema(); - // if ( ! empty( $schema['properties']['sticky'] ) ) { - // if ( ! empty( $request['sticky'] ) ) { - // stick_post( $post_id ); - // } else { - // unstick_post( $post_id ); - // } - // } - - // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { - // $this->handle_featured_media( $request['featured_media'], $post->ID ); - // } - - // $terms_update = $this->handle_terms( $post->ID, $request ); - // if ( is_wp_error( $terms_update ) ) { - // return $terms_update; - // } - $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); @@ -272,23 +255,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $schema = $this->get_item_schema(); - // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { - // $this->handle_featured_media( $request['featured_media'], $post_id ); - // } - - // if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) { - // if ( ! empty( $request['sticky'] ) ) { - // stick_post( $post_id ); - // } else { - // unstick_post( $post_id ); - // } - // } - - // $terms_update = $this->handle_terms( $post->ID, $request ); - // if ( is_wp_error( $terms_update ) ) { - // return $terms_update; - // } - $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php index 84ec69865ee..3dc79adffb4 100644 --- a/includes/api/class-wc-rest-report-sales-controller.php +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -65,8 +65,8 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list reports.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-reports-controller.php b/includes/api/class-wc-rest-reports-controller.php index 79441b54635..4d6cea4b417 100644 --- a/includes/api/class-wc-rest-reports-controller.php +++ b/includes/api/class-wc-rest-reports-controller.php @@ -58,8 +58,8 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list reports.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php index 404d133cf3c..76e8b85ea7b 100644 --- a/includes/api/class-wc-rest-tax-classes-controller.php +++ b/includes/api/class-wc-rest-tax-classes-controller.php @@ -79,7 +79,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list tax classes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -93,7 +93,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -107,7 +107,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * @return boolean */ public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/api/class-wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php index 9e9f8bc8335..72a6e1913da 100644 --- a/includes/api/class-wc-rest-taxes-controller.php +++ b/includes/api/class-wc-rest-taxes-controller.php @@ -93,7 +93,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list taxes.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -107,7 +107,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -121,7 +121,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -135,7 +135,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return boolean */ public function update_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -149,7 +149,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * @return boolean */ public function delete_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_woocommerce' ) ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 564b21c3f7a..ad037d29ec5 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -272,3 +272,23 @@ function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $ return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); } + +/** + * Check manager permissions on REST API. + * + * @since 2.6.0 + * @param string $object Object. + * @param string $context Request context. + * @return bool + */ +function wc_rest_check_manager_permissions( $object, $context = 'read' ) { + $objects = array( + 'reports' => 'view_woocommerce_reports', + 'settings' => 'manage_woocommerce', + ); + + $permission = current_user_can( $objects[ $object ] ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); +} + From f574a149d157274929f38eed25ad01f3c9f413d4 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:49:22 -0300 Subject: [PATCH 129/177] Attributes permissions --- .../abstract-wc-rest-terms-controller.php | 2 +- ...-wc-rest-product-attributes-controller.php | 28 +++++++++++-------- includes/wc-rest-functions.php | 5 ++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 254e83358f8..1f5ccaa7545 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -134,7 +134,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $term = get_term( (int) $request['id'], $taxonomy ); if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'read', $term->term_id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php index 679a984c48e..f82f694f76a 100644 --- a/includes/api/class-wc-rest-product-attributes-controller.php +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -104,8 +104,8 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( 'edit' === $request['context'] && ! current_user_can( 'manage_product_terms' ) ) { - return new WP_Error( 'woocommerce_rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -118,7 +118,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { - if ( ! current_user_can( 'manage_product_terms' ) ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -132,7 +132,15 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - return $this->get_items_permissions_check( $request ); + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; } /** @@ -142,13 +150,11 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { + if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -162,13 +168,11 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { + if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( "woocommerce_rest_taxonomy_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $taxonomy_obj = get_taxonomy( $taxonomy ); - if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index ad037d29ec5..10ca418462b 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -283,8 +283,9 @@ function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $ */ function wc_rest_check_manager_permissions( $object, $context = 'read' ) { $objects = array( - 'reports' => 'view_woocommerce_reports', - 'settings' => 'manage_woocommerce', + 'reports' => 'view_woocommerce_reports', + 'settings' => 'manage_woocommerce', + 'attributes' => 'manage_product_terms', ); $permission = current_user_can( $objects[ $object ] ); From 8e9e9526acee8f630164931dad756f0e229bc856 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:53:46 -0300 Subject: [PATCH 130/177] Fixed date params --- .../api/class-wc-rest-coupons-controller.php | 8 +++---- .../class-wc-rest-customers-controller.php | 8 +++---- .../class-wc-rest-order-notes-controller.php | 4 ++-- .../api/class-wc-rest-webhook-deliveries.php | 4 ++-- .../api/class-wc-rest-webhooks-controller.php | 24 +++++++++---------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index c42c7201c9c..8b34c54bcfd 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -116,8 +116,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, - 'created_at' => wc_rest_prepare_date_response( $post->post_date_gmt ), - 'updated_at' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), @@ -412,13 +412,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'enum' => array_keys( wc_get_coupon_types() ), 'context' => array( 'view', 'edit' ), ), - 'created_at' => array( + 'date_created' => array( 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'updated_at' => array( + 'date_modified' => array( 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index 7a6529a2617..2c25b394002 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -480,8 +480,8 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $data = array( 'id' => $customer->ID, - 'created_at' => wc_rest_prepare_date_response( $customer->user_registered ), - 'updated_at' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, + 'date_created' => wc_rest_prepare_date_response( $customer->user_registered ), + 'date_modified' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, @@ -612,13 +612,13 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'created_at' => array( + 'date_created' => array( 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'updated_at' => array( + 'date_modified' => array( 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 7566946d4f4..722a28a4228 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -311,7 +311,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { public function prepare_item_for_response( $note, $request ) { $data = array( 'id' => $note->comment_ID, - 'created_at' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), ); @@ -377,7 +377,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'created_at' => array( + 'date_created' => array( 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index 94a143b52cd..4d9e4a059bf 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -156,7 +156,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { $data = (array) $log; // Add timestamp. - $data['created_at'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); + $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); // Remove comment object. unset( $data['comment'] ); @@ -284,7 +284,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { 'context' => array( 'view' ), 'readonly' => true, ), - 'created_at' => array( + 'date_created' => array( 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index 4cc70661ea4..d2a1397daa8 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -389,16 +389,16 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { $webhook = new WC_Webhook( $id ); $data = array( - 'id' => $webhook->id, - 'name' => $webhook->get_name(), - 'status' => $webhook->get_status(), - 'topic' => $webhook->get_topic(), - 'resource' => $webhook->get_resource(), - 'event' => $webhook->get_event(), - 'hooks' => $webhook->get_hooks(), - 'delivery_url' => $webhook->get_delivery_url(), - 'created_at' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), - 'updated_at' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + 'id' => $webhook->id, + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -516,13 +516,13 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { 'context' => array( 'edit' ), 'writeonly' => true, ), - 'created_at' => array( + 'date_created' => array( 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'updated_at' => array( + 'date_modified' => array( 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), From 7676e856403c8fba1c0773b689cc1bdfa118b075 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:55:43 -0300 Subject: [PATCH 131/177] Fixed vendor wp rest functions file name --- includes/class-wc-api.php | 2 +- includes/vendor/{wp-api-functions.php => wp-rest-functions.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename includes/vendor/{wp-api-functions.php => wp-rest-functions.php} (100%) diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index bee17234ccc..5111c19948d 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -373,7 +373,7 @@ class WC_API { include_once( 'api/class-wc-rest-authentication.php' ); // WP-API classes and functions. - include_once( 'vendor/wp-api-functions.php' ); + include_once( 'vendor/wp-rest-functions.php' ); if ( ! class_exists( 'WP_REST_Controller' ) ) { include_once( 'vendor/class-wp-rest-controller.php' ); } diff --git a/includes/vendor/wp-api-functions.php b/includes/vendor/wp-rest-functions.php similarity index 100% rename from includes/vendor/wp-api-functions.php rename to includes/vendor/wp-rest-functions.php From fda9defef8390b5aa7d232ccd9f5c6d7ca7040b3 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 14:56:31 -0300 Subject: [PATCH 132/177] Fixed textdomains --- includes/vendor/wp-rest-functions.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/includes/vendor/wp-rest-functions.php b/includes/vendor/wp-rest-functions.php index 20dd2e74b8a..c5d1a4a6f3b 100644 --- a/includes/vendor/wp-rest-functions.php +++ b/includes/vendor/wp-rest-functions.php @@ -89,29 +89,29 @@ if ( ! function_exists( 'rest_validate_request_arg' ) ) { if ( ! empty( $args['enum'] ) ) { if ( ! in_array( $value, $args['enum'] ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s' ), $param, implode( ', ', $args['enum'] ) ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not one of %s', 'woocommerce' ), $param, implode( ', ', $args['enum'] ) ) ); } } if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $param, 'integer' ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'integer' ) ); } if ( 'string' === $args['type'] && ! is_string( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s' ), $param, 'string' ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'string' ) ); } if ( isset( $args['format'] ) ) { switch ( $args['format'] ) { case 'date-time' : if ( ! rest_parse_date( $value ) ) { - return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) ); + return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); } break; case 'email' : if ( ! is_email( $value ) ) { - return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) ); + return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.', 'woocommerce' ) ); } break; } @@ -120,32 +120,32 @@ if ( ! function_exists( 'rest_validate_request_arg' ) ) { if ( in_array( $args['type'], array( 'numeric', 'integer' ) ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) { if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (exclusive)' ), $param, $args['minimum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (exclusive)', 'woocommerce' ), $param, $args['minimum'] ) ); } else if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (inclusive)' ), $param, $args['minimum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be greater than %d (inclusive)', 'woocommerce' ), $param, $args['minimum'] ) ); } } else if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (exclusive)' ), $param, $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (exclusive)', 'woocommerce' ), $param, $args['maximum'] ) ); } else if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (inclusive)' ), $param, $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be less than %d (inclusive)', 'woocommerce' ), $param, $args['maximum'] ) ); } } else if ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (exclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); } } else if ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (exclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); } } else if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (exclusive) and %d (inclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); } } else if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { if ( $value > $args['maximum'] || $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s must be between %d (inclusive) and %d (inclusive)', 'woocommerce' ), $param, $args['minimum'], $args['maximum'] ) ); } } } From 0cd16b3cc06268c3ebfa3fa1fc7229caa9d4cc58 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 17:34:36 -0300 Subject: [PATCH 133/177] Fixed file description --- .../api/class-wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/class-wc-rest-webhook-deliveries.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/api/class-wc-rest-product-attribute-terms-controller.php b/includes/api/class-wc-rest-product-attribute-terms-controller.php index 3946bf3b1ea..e9e45758971 100644 --- a/includes/api/class-wc-rest-product-attribute-terms-controller.php +++ b/includes/api/class-wc-rest-product-attribute-terms-controller.php @@ -2,7 +2,7 @@ /** * REST API Product Attribute Terms controller * - * Handles requests to the products/attributes/(?P[\d]+)/terms endpoint. + * Handles requests to the products/attributes//terms endpoint. * * @author WooThemes * @category API diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index 4d9e4a059bf..355142a73f6 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -2,7 +2,7 @@ /** * REST API Webhooks controller * - * Handles requests to the /webhooks/(?P[\d]+)/deliveries endpoint. + * Handles requests to the /webhooks//deliveries endpoint. * * @author WooThemes * @category API From f76f0d84720acb757215390139f54617a13fc1ae Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 17:39:20 -0300 Subject: [PATCH 134/177] Check permissions in customers/me endpoint --- includes/api/class-wc-rest-customers-controller.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index 2c25b394002..b2c675d2f6e 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -454,15 +454,19 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return WP_Error|WP_REST_Response */ public function get_current_item( $request ) { - $current_customer_id = get_current_user_id(); - if ( empty( $current_customer_id ) ) { + $id = get_current_user_id(); + if ( empty( $id ) ) { return new WP_Error( 'woocommerce_rest_not_logged_in', __( 'You are not currently logged in.', 'woocommerce' ), array( 'status' => 401 ) ); } + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + $customer = wp_get_current_user(); $response = $this->prepare_item_for_response( $customer, $request ); $response = rest_ensure_response( $response ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $current_customer_id ) ) ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) ); $response->set_status( 302 ); return $response; From f78a2ec68bf0e4188d9d9a5bc12e5c9d12a458d6 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 18:12:34 -0300 Subject: [PATCH 135/177] Added endpoint to list customer downloads --- ...-wc-rest-customer-downloads-controller.php | 244 ++++++++++++++++++ includes/class-wc-api.php | 2 + 2 files changed, 246 insertions(+) create mode 100644 includes/api/class-wc-rest-customer-downloads-controller.php diff --git a/includes/api/class-wc-rest-customer-downloads-controller.php b/includes/api/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..15ff968a5a3 --- /dev/null +++ b/includes/api/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,244 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Customer_Downloads_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + public $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'customers/(?P[\d]+)/downloads'; + + /** + * Register the routes for customers. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $customer = get_user_by( 'id', (int) $request['customer_id'] ); + + if ( ! $customer ) { + return new WP_Error( "woocommerce_rest_customer_invalid", __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_user_permissions( 'read', $customer->id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customer downloads. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] ); + + $data = array(); + foreach ( $downloads as $download_data ) { + $download = $this->prepare_item_for_response( (object) $download_data, $request ); + $download = $this->prepare_response_for_collection( $download ); + $data[] = $download; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single download output for response. + * + * @param stdObject $download Download object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $download, $request ) { + $data = (array) $download; + $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; + $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $download, $request ) ); + + /** + * Filter customer download data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdObject $download Download object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $download Download object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given customer download. + */ + protected function prepare_links( $download, $request ) { + $base = str_replace( '(?P[\d]+)', $request['customer_id'], $this->rest_base ); + + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ), + ), + 'order' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer Download's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer_download', + 'type' => 'object', + 'properties' => array( + 'download_url' => array( + 'description' => __( 'Download file URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_id' => array( + 'description' => __( 'Download ID (MD5).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Downloadable product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_name' => array( + 'description' => __( 'Downloadable file name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_id' => array( + 'description' => __( 'Order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'downloads_remaining' => array( + 'description' => __( 'Amount of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when the download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 5111c19948d..0e322d4baf3 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -384,6 +384,7 @@ class WC_API { // REST API controllers. include_once( 'api/class-wc-rest-coupons-controller.php' ); + include_once( 'api/class-wc-rest-customer-downloads-controller.php' ); include_once( 'api/class-wc-rest-customers-controller.php' ); include_once( 'api/class-wc-rest-order-notes-controller.php' ); include_once( 'api/class-wc-rest-order-refunds-controller.php' ); @@ -411,6 +412,7 @@ class WC_API { public function register_rest_routes() { $controllers = array( 'WC_REST_Coupons_Controller', + 'WC_REST_Customer_Downloads_Controller', 'WC_REST_Customers_Controller', 'WC_REST_Order_Notes_Controller', 'WC_REST_Order_Refunds_Controller', From 1722f95a95591b2d7007dda1a012b563e710b5d1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 18:33:37 -0300 Subject: [PATCH 136/177] Allow filter customers by role or email --- .../class-wc-rest-customers-controller.php | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index b2c675d2f6e..973e4da20e1 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -212,13 +212,16 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } - if ( ! empty( $request['slug'] ) ) { - $prepared_args['search'] = $request['slug']; - $prepared_args['search_columns'] = array( 'user_nicename' ); + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); } - // Show only customers. - $prepared_args['role'] = 'customer'; + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } /** * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. @@ -822,6 +825,17 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { return $this->add_additional_fields_schema( $schema ); } + /** + * Get role names. + * + * @return array + */ + protected function get_role_names() { + global $wp_roles; + + return array_keys( $wp_roles->role_names ); + } + /** * Get the query params for collections. * @@ -871,9 +885,17 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['slug'] = array( - 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), 'validate_callback' => 'rest_validate_request_arg', ); return $params; From 9619c041cda4075ad7757e497ba35c58eec3de48 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 18:42:51 -0300 Subject: [PATCH 137/177] Allow filter coupons by code --- .../api/class-wc-rest-coupons-controller.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 8b34c54bcfd..a172f0b2802 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -43,6 +43,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'shop_coupon'; + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + /** * Register the routes for coupons. */ @@ -97,6 +104,25 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { ) ); } + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + if ( ! empty( $request['code'] ) ) { + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $request['code'] ) ); + + $args['post__in'] = array( $id ); + } + + return $args; + } + /** * Prepare a single coupon output for response. * @@ -515,4 +541,22 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { return $this->add_additional_fields_schema( $schema );; } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } } From 5d99b8b336a20d96a4d56636a98717734ee23bee Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 19:11:56 -0300 Subject: [PATCH 138/177] Register dp filter for orders and allow filter orders by customer id --- .../abstract-wc-rest-posts-controller.php | 1 + ...class-wc-rest-order-refunds-controller.php | 21 +++++++- .../api/class-wc-rest-orders-controller.php | 53 ++++++++++--------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 2ec2904c374..8280602542c 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -555,6 +555,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { 'post_parent__in', 'post_parent__not_in', 'posts_per_page', + 'meta_query', ); $valid_vars = array_merge( $valid_vars, $rest_valid ); diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index 3f772b6fbaf..0575fc6133c 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -122,7 +122,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); } - $dp = ! empty( $request['dp'] ) ? intval( $request['dp'] ) : 2; + $dp = $request['dp']; $data = array( 'id' => $refund->id, @@ -506,4 +506,23 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { return $this->add_additional_fields_schema( $schema ); } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } } diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 2c18a7ca141..a839ae3e404 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -112,7 +112,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { global $wpdb; $order = wc_get_order( $post ); - $dp = ! empty( $request['dp'] ) ? intval( $request['dp'] ) : 2; + $dp = $request['dp']; $data = array( 'id' => $order->id, @@ -219,14 +219,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { $line_item['taxes'] = array_values( $line_tax ); } - // if ( in_array( 'products', $expand ) ) { - // $_product_data = WC()->api->WC_API_Products->get_product( $product_id ); - - // if ( isset( $_product_data['product'] ) ) { - // $line_item['product_data'] = $_product_data['product']; - // } - // } - $data['line_items'][] = $line_item; } @@ -242,14 +234,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), ); - // if ( in_array( 'taxes', $expand ) ) { - // $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); - - // if ( isset( $_rate_data['tax'] ) ) { - // $tax_line['rate_data'] = $_rate_data['tax']; - // } - // } - $data['tax_lines'][] = $tax_line; } @@ -323,14 +307,6 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), ); - // if ( in_array( 'coupons', $expand ) ) { - // $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item['name'] ); - - // if ( isset( $_coupon_data['coupon'] ) ) { - // $coupon_line['coupon_data'] = $_coupon_data['coupon']; - // } - // } - $data['coupon_lines'][] = $coupon_line; } @@ -402,6 +378,18 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { $args['post_status'] = 'any'; } + if ( ! empty( $request['customer_id'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer_id'], + 'type' => 'NUMERIC', + ); + } + return $args; } @@ -1667,7 +1655,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { } /** - * Get the query params for collections of attachments. + * Get the query params for collections. * * @return array */ @@ -1682,6 +1670,19 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); + $params['customer_id'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } From bb7e5ecfb6c120405f0d8addf1153844f3d9bf28 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 20:05:12 -0300 Subject: [PATCH 139/177] Allow filter products by taxonomies and sku --- .../abstract-wc-rest-posts-controller.php | 1 + .../api/class-wc-rest-products-controller.php | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 8280602542c..d8f9a951fc4 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -556,6 +556,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { 'post_parent__not_in', 'posts_per_page', 'meta_query', + 'tax_query', ); $valid_vars = array_merge( $valid_vars, $rest_valid ); diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index e5835b9f053..3117e29b229 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -113,6 +113,63 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Set post_status. $args['post_status'] = $request['status']; + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_type' => 'type', + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $terms = explode( ',', $request[ $key ] ); + + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names() ) ) { + $terms = explode( ',', $request['attribute_term'] ); + + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $request['sku'], + 'compare' => '=' + ); + + $args['post_type'] = array( 'product', 'product_variation' ); + } + return $args; } @@ -2435,6 +2492,49 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } From 7dcfcc9dfc580e440d14a011319e56fbf1a3d25f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 20:26:51 -0300 Subject: [PATCH 140/177] Allow filter orders by product --- .../api/class-wc-rest-orders-controller.php | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index a839ae3e404..54509bb798d 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -371,6 +371,8 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * @return array */ public function query_args( $args, $request ) { + global $wpdb; + // Set post_status. if ( 'any' !== $request['status'] ) { $args['post_status'] = 'wc-' . $request['status']; @@ -378,18 +380,32 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { $args['post_status'] = 'any'; } - if ( ! empty( $request['customer_id'] ) ) { + if ( ! empty( $request['customer'] ) ) { if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = array(); } $args['meta_query'][] = array( 'key' => '_customer_user', - 'value' => $request['customer_id'], + 'value' => $request['customer'], 'type' => 'NUMERIC', ); } + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + return $args; } @@ -1670,12 +1686,18 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); - $params['customer_id'] = array( + $params['customer'] = array( 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); $params['dp'] = array( 'default' => 2, 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), From 7b508307f08d0d0b7d7a62cf01dd7db176457642 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 20:42:06 -0300 Subject: [PATCH 141/177] Fixed links for webhooks deliveries and order notes --- includes/api/class-wc-rest-order-notes-controller.php | 2 +- includes/api/class-wc-rest-webhook-deliveries.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 722a28a4228..a3f19db9f63 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -353,7 +353,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( - 'href' => rest_url( sprintf( '/wc/v1/orders/%d', $order_id ) ), + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), ), ); diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index 355142a73f6..e0e3b6212b0 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -100,7 +100,6 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { * @return array */ public function get_items( $request ) { - $id = (int) $request['id']; $webhook = new WC_Webhook( (int) $request['webhook_id'] ); if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { @@ -198,7 +197,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( - 'href' => rest_url( sprintf( '/wc/v1/webhooks/%d', $webhook_id ) ), + 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), ), ); From b8dd1568915cb873d5a89a259fd37fa7075aa2e5 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Wed, 30 Mar 2016 20:50:06 -0300 Subject: [PATCH 142/177] Added endpoint to list and get product reviews --- ...ass-wc-rest-product-reviews-controller.php | 273 ++++++++++++++++++ includes/class-wc-api.php | 2 + 2 files changed, 275 insertions(+) create mode 100644 includes/api/class-wc-rest-product-reviews-controller.php diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..4a51fdb04ab --- /dev/null +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,273 @@ +/reviews endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Products controller class. + * + * @package WooCommerce/API + * @extends WP_REST_Controller + */ +class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + public $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @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_post_permissions( 'product', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook develivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $product = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $reviews = get_approved_comments( $product->ID ); + + $data = array(); + foreach ( $reviews as $review_data ) { + $review = $this->prepare_item_for_response( $review_data, $request ); + $review = $this->prepare_response_for_collection( $review ); + $data[] = $review; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $product = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $id ); + + if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== intval( $product->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => get_comment_meta( $review->comment_ID, 'rating', true ), + 'reviewer_name' => $review->comment_author, + 'reviewer_email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'reviewer_name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'reviewer_email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 0e322d4baf3..c4907860161 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -392,6 +392,7 @@ class WC_API { include_once( 'api/class-wc-rest-product-attribute-terms-controller.php' ); include_once( 'api/class-wc-rest-product-attributes-controller.php' ); include_once( 'api/class-wc-rest-product-categories-controller.php' ); + include_once( 'api/class-wc-rest-product-reviews-controller.php' ); include_once( 'api/class-wc-rest-product-shipping-classes-controller.php' ); include_once( 'api/class-wc-rest-product-tags-controller.php' ); include_once( 'api/class-wc-rest-products-controller.php' ); @@ -420,6 +421,7 @@ class WC_API { 'WC_REST_Product_Attribute_Terms_Controller', 'WC_REST_Product_Attributes_Controller', 'WC_REST_Product_Categories_Controller', + 'WC_REST_Product_Reviews_Controller', 'WC_REST_Product_Shipping_Classes_Controller', 'WC_REST_Product_Tags_Controller', 'WC_REST_Products_Controller', From 00a1a9c3da5dba5b3430db2db3a298d7c9d6074a Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 09:56:22 -0300 Subject: [PATCH 143/177] Fixed backorders and stock status --- .../api/class-wc-rest-products-controller.php | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 3117e29b229..dd3569ab8fd 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -1103,12 +1103,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Backorders. if ( isset( $request['backorders'] ) ) { - if ( 'notify' === $request['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $request['backorders'] ) ? 'yes' : 'no'; - } - + $backorders = $request['backorders']; update_post_meta( $product->id, '_backorders', $backorders ); } else { $backorders = get_post_meta( $product->id, '_backorders', true ); @@ -1126,6 +1121,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { update_post_meta( $product->id, '_stock', '' ); wc_update_product_stock_status( $product->id, 'instock' ); + } elseif ( 'variable' === $product_type ) { + update_post_meta( $product->id, '_stock', '' ); } elseif ( 'yes' == $managing_stock ) { update_post_meta( $product->id, '_backorders', $backorders ); @@ -1149,7 +1146,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { wc_update_product_stock_status( $product->id, $stock_status ); } - } else { + } elseif ( 'variable' !== $product_type ) { wc_update_product_stock_status( $product->id, $stock_status ); } @@ -1370,27 +1367,25 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Stock handling. if ( isset( $variation['managing_stock'] ) ) { $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; - update_post_meta( $variation_id, '_manage_stock', $managing_stock ); } else { $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); } - // Only update stock status to user setting if changed by the user, - // but do so before looking at stock levels at variation level. + update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock ); + if ( isset( $variation['in_stock'] ) ) { $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; - wc_update_product_stock_status( $variation_id, $stock_status ); + } else { + $stock_status = get_post_meta( $variation_id, '_stock_status', true ); } + wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); + if ( 'yes' === $managing_stock ) { $backorders = get_post_meta( $variation_id, '_backorders', true ); if ( isset( $variation['backorders'] ) ) { - if ( 'notify' == $variation['backorders'] ) { - $backorders = 'notify'; - } else { - $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; - } + $backorders = $variation['backorders']; } update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); @@ -1528,7 +1523,6 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Update parent if variable so price sorting works and stays in sync with the cheapest child. WC_Product_Variable::sync( $product->id ); - WC_Product_Variable::sync_stock_status( $product->id ); // Update default attributes options setting. if ( isset( $request['default_attribute'] ) ) { From 92da79ce76213ca0dd9ca90ea2d341da472206fd Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 15:25:31 -0300 Subject: [PATCH 144/177] Changed $namespace from public to protected --- includes/api/class-wc-rest-coupons-controller.php | 2 +- includes/api/class-wc-rest-customer-downloads-controller.php | 2 +- includes/api/class-wc-rest-customers-controller.php | 2 +- includes/api/class-wc-rest-order-notes-controller.php | 2 +- includes/api/class-wc-rest-order-refunds-controller.php | 2 +- includes/api/class-wc-rest-orders-controller.php | 2 +- .../api/class-wc-rest-product-attribute-terms-controller.php | 2 +- includes/api/class-wc-rest-product-attributes-controller.php | 2 +- includes/api/class-wc-rest-product-categories-controller.php | 2 +- includes/api/class-wc-rest-product-reviews-controller.php | 2 +- .../api/class-wc-rest-product-shipping-classes-controller.php | 2 +- includes/api/class-wc-rest-product-tags-controller.php | 2 +- includes/api/class-wc-rest-products-controller.php | 2 +- includes/api/class-wc-rest-report-sales-controller.php | 2 +- includes/api/class-wc-rest-report-top-sellers-controller.php | 2 +- includes/api/class-wc-rest-reports-controller.php | 2 +- includes/api/class-wc-rest-tax-classes-controller.php | 2 +- includes/api/class-wc-rest-taxes-controller.php | 2 +- includes/api/class-wc-rest-webhook-deliveries.php | 2 +- includes/api/class-wc-rest-webhooks-controller.php | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index a172f0b2802..3bce1f415d1 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -27,7 +27,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-customer-downloads-controller.php b/includes/api/class-wc-rest-customer-downloads-controller.php index 15ff968a5a3..05327523a11 100644 --- a/includes/api/class-wc-rest-customer-downloads-controller.php +++ b/includes/api/class-wc-rest-customer-downloads-controller.php @@ -27,7 +27,7 @@ class WC_REST_Customer_Downloads_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index 973e4da20e1..aed42d8513b 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -27,7 +27,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index a3f19db9f63..9cb3f28dca6 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index 0575fc6133c..ef28afc263a 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -27,7 +27,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 54509bb798d..813b5d67583 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -27,7 +27,7 @@ class WC_REST_Orders_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-attribute-terms-controller.php b/includes/api/class-wc-rest-product-attribute-terms-controller.php index e9e45758971..4941c9cf773 100644 --- a/includes/api/class-wc-rest-product-attribute-terms-controller.php +++ b/includes/api/class-wc-rest-product-attribute-terms-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Terms_Controlle * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php index f82f694f76a..f19a3df42a8 100644 --- a/includes/api/class-wc-rest-product-attributes-controller.php +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-categories-controller.php b/includes/api/class-wc-rest-product-categories-controller.php index 18b490bdce9..a9194844859 100644 --- a/includes/api/class-wc-rest-product-categories-controller.php +++ b/includes/api/class-wc-rest-product-categories-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Categories_Controller extends WC_REST_Terms_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php index 4a51fdb04ab..d6301850815 100644 --- a/includes/api/class-wc-rest-product-reviews-controller.php +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-shipping-classes-controller.php b/includes/api/class-wc-rest-product-shipping-classes-controller.php index 008cf0df7a2..ddd5121208d 100644 --- a/includes/api/class-wc-rest-product-shipping-classes-controller.php +++ b/includes/api/class-wc-rest-product-shipping-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Terms_Controll * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-product-tags-controller.php b/includes/api/class-wc-rest-product-tags-controller.php index 230653d879f..494de9526b2 100644 --- a/includes/api/class-wc-rest-product-tags-controller.php +++ b/includes/api/class-wc-rest-product-tags-controller.php @@ -27,7 +27,7 @@ class WC_REST_Product_Tags_Controller extends WC_REST_Terms_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index dd3569ab8fd..69a1a66d5dc 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -27,7 +27,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php index 3dc79adffb4..339eaf0818b 100644 --- a/includes/api/class-wc-rest-report-sales-controller.php +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -27,7 +27,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-report-top-sellers-controller.php b/includes/api/class-wc-rest-report-top-sellers-controller.php index 6c7bb6b791a..e1ddf7c2804 100644 --- a/includes/api/class-wc-rest-report-top-sellers-controller.php +++ b/includes/api/class-wc-rest-report-top-sellers-controller.php @@ -27,7 +27,7 @@ class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Sales_Control * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-reports-controller.php b/includes/api/class-wc-rest-reports-controller.php index 4d6cea4b417..24d0f8b4ff9 100644 --- a/includes/api/class-wc-rest-reports-controller.php +++ b/includes/api/class-wc-rest-reports-controller.php @@ -27,7 +27,7 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php index 76e8b85ea7b..ee5063ff942 100644 --- a/includes/api/class-wc-rest-tax-classes-controller.php +++ b/includes/api/class-wc-rest-tax-classes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php index 72a6e1913da..1761a7cadc5 100644 --- a/includes/api/class-wc-rest-taxes-controller.php +++ b/includes/api/class-wc-rest-taxes-controller.php @@ -27,7 +27,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index e0e3b6212b0..96facb7079d 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -27,7 +27,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index d2a1397daa8..9c59a7091bb 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -27,7 +27,7 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. From bb029747b775a2f80a47923f5e5b70034d2f5287 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 15:28:17 -0300 Subject: [PATCH 145/177] Changed $namespace from public to protected in posts controller --- includes/abstracts/abstract-wc-rest-posts-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index d8f9a951fc4..68b604c2af6 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -19,7 +19,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * * @var string */ - public $namespace = 'wc/v1'; + protected $namespace = 'wc/v1'; /** * Route base. From 8df7723895eead578db3e1c1e66245ff2d83f00f Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:03:59 -0300 Subject: [PATCH 146/177] Fixed permissions error messages --- includes/abstracts/abstract-wc-rest-posts-controller.php | 4 ++-- includes/api/class-wc-rest-customers-controller.php | 4 ++-- includes/api/class-wc-rest-order-notes-controller.php | 2 +- includes/api/class-wc-rest-tax-classes-controller.php | 2 +- includes/api/class-wc-rest-taxes-controller.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 68b604c2af6..5d9319428e9 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -64,7 +64,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -96,7 +96,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $post = get_post( $request['id'] ); if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index aed42d8513b..b2c5db6ec1d 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -128,7 +128,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_user_permissions( 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -160,7 +160,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { $id = (int) $request['id']; if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 9cb3f28dca6..9f317e35de3 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -106,7 +106,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_post_permissions( 'shop_order', 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-tax-classes-controller.php b/includes/api/class-wc-rest-tax-classes-controller.php index ee5063ff942..046090f275b 100644 --- a/includes/api/class-wc-rest-tax-classes-controller.php +++ b/includes/api/class-wc-rest-tax-classes-controller.php @@ -94,7 +94,7 @@ class WC_REST_Tax_Classes_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/includes/api/class-wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php index 1761a7cadc5..74629259357 100644 --- a/includes/api/class-wc-rest-taxes-controller.php +++ b/includes/api/class-wc-rest-taxes-controller.php @@ -108,7 +108,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { */ public function create_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -136,7 +136,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { */ public function update_item_permissions_check( $request ) { if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; From bf64942b28b26288ea018175b9577420990b6fc1 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:14:18 -0300 Subject: [PATCH 147/177] Align variables --- .../abstract-wc-rest-posts-controller.php | 8 +++----- .../abstract-wc-rest-terms-controller.php | 5 ++--- includes/api/class-wc-rest-authentication.php | 17 +++++++---------- ...ss-wc-rest-customer-downloads-controller.php | 5 ++--- .../api/class-wc-rest-customers-controller.php | 6 +++--- .../class-wc-rest-order-notes-controller.php | 3 +-- .../class-wc-rest-order-refunds-controller.php | 13 ++----------- ...ss-wc-rest-product-attributes-controller.php | 6 ++---- ...class-wc-rest-product-reviews-controller.php | 6 ++---- .../api/class-wc-rest-products-controller.php | 3 +-- .../api/class-wc-rest-webhook-deliveries.php | 3 +-- .../api/class-wc-rest-webhooks-controller.php | 3 +-- 12 files changed, 27 insertions(+), 51 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 5d9319428e9..1114cf68956 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -231,7 +231,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { - $id = (int) $request['id']; + $id = (int) $request['id']; $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { @@ -391,10 +391,9 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { - $id = (int) $request['id']; + $id = (int) $request['id']; $force = (bool) $request['force']; - - $post = get_post( $id ); + $post = get_post( $id ); if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); @@ -473,7 +472,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $links; } - /** * Determine the allowed query_vars for a get_items() response and * prepare for WP_Query. diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 1f5ccaa7545..3ca2010256f 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -187,8 +187,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - + $taxonomy = $this->get_taxonomy( $request ); $prepared_args = array( 'exclude' => $request['exclude'], 'include' => $request['include'], @@ -300,8 +299,8 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $taxonomy = $this->get_taxonomy( $request ); $name = $request['name']; $args = array(); + $schema = $this->get_item_schema(); - $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { $args['description'] = $request['description']; } diff --git a/includes/api/class-wc-rest-authentication.php b/includes/api/class-wc-rest-authentication.php index 75c666cf5f3..fe72a038371 100644 --- a/includes/api/class-wc-rest-authentication.php +++ b/includes/api/class-wc-rest-authentication.php @@ -187,8 +187,7 @@ class WC_REST_Authentication { * @return null|WP_Error */ private function check_oauth_signature( $user, $params ) { - $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); - + $http_method = strtoupper( $_SERVER['REQUEST_METHOD'] ); $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); $wp_base = get_home_url( null, '/', 'relative' ); if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { @@ -206,7 +205,7 @@ class WC_REST_Authentication { } // Normalize parameter key/values. - $params = $this->normalize_parameters( $params ); + $params = $this->normalize_parameters( $params ); $query_parameters = array(); foreach ( $params as $param_key => $param_value ) { if ( is_array( $param_value ) ) { @@ -217,8 +216,7 @@ class WC_REST_Authentication { $query_parameters[] = $param_key . '%3D' . $param_value; // Join with equals sign. } } - $query_string = implode( '%26', $query_parameters ); // Join with ampersand. - + $query_string = implode( '%26', $query_parameters ); // Join with ampersand. $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string; if ( $params['oauth_signature_method'] !== 'HMAC-SHA1' && $params['oauth_signature_method'] !== 'HMAC-SHA256' ) { @@ -226,9 +224,8 @@ class WC_REST_Authentication { } $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); - - $secret = $user->consumer_secret . '&'; - $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); + $secret = $user->consumer_secret . '&'; + $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); if ( ! hash_equals( $signature, $consumer_signature ) ) { return new WP_Error( 'woocommerce_rest_authentication_error', __( 'Invalid Signature - provided signature does not match.', 'woocommerce' ), array( 'status' => 401 ) ); @@ -257,8 +254,8 @@ class WC_REST_Authentication { * @return array Normalized parameters. */ private function normalize_parameters( $parameters ) { - $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); - $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); + $keys = wc_rest_urlencode_rfc3986( array_keys( $parameters ) ); + $values = wc_rest_urlencode_rfc3986( array_values( $parameters ) ); $parameters = array_combine( $keys, $values ); return $parameters; diff --git a/includes/api/class-wc-rest-customer-downloads-controller.php b/includes/api/class-wc-rest-customer-downloads-controller.php index 05327523a11..2b5deed07d4 100644 --- a/includes/api/class-wc-rest-customer-downloads-controller.php +++ b/includes/api/class-wc-rest-customer-downloads-controller.php @@ -99,7 +99,7 @@ class WC_REST_Customer_Downloads_Controller extends WP_REST_Controller { */ public function prepare_item_for_response( $download, $request ) { $data = (array) $download; - $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; + $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -129,8 +129,7 @@ class WC_REST_Customer_Downloads_Controller extends WP_REST_Controller { * @return array Links for the given customer download. */ protected function prepare_links( $download, $request ) { - $base = str_replace( '(?P[\d]+)', $request['customer_id'], $this->rest_base ); - + $base = str_replace( '(?P[\d]+)', $request['customer_id'], $this->rest_base ); $links = array( 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), diff --git a/includes/api/class-wc-rest-customers-controller.php b/includes/api/class-wc-rest-customers-controller.php index b2c5db6ec1d..0b3a6ecde4c 100644 --- a/includes/api/class-wc-rest-customers-controller.php +++ b/includes/api/class-wc-rest-customers-controller.php @@ -214,7 +214,7 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { // Filter by email. if ( ! empty( $request['email'] ) ) { - $prepared_args['search'] = $request['email']; + $prepared_args['search'] = $request['email']; $prepared_args['search_columns'] = array( 'user_email' ); } @@ -354,9 +354,9 @@ class WC_REST_Customers_Controller extends WP_REST_Controller { * @return WP_Error|WP_REST_Response */ public function update_item( $request ) { - $id = (int) $request['id']; - + $id = (int) $request['id']; $customer = get_userdata( $id ); + if ( ! $customer ) { return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); } diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 9f317e35de3..09fea42c28a 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -344,8 +344,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { protected function prepare_links( $note ) { $order_id = (int) $note->comment_post_ID; $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); - - $links = array( + $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), ), diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index ef28afc263a..36ce2c2dd19 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -196,14 +196,6 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { $line_item['taxes'] = array_values( $line_tax ); } - // if ( in_array( 'products', $expand ) ) { - // $_product_data = WC()->api->WC_API_Products->get_product( $product_id ); - - // if ( isset( $_product_data['product'] ) ) { - // $line_item['product_data'] = $_product_data['product']; - // } - // } - $data['line_items'][] = $line_item; } @@ -237,9 +229,8 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { */ protected function prepare_links( $refund ) { $order_id = $refund->post->post_parent; - $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); - - $links = array( + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->id ) ), ), diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php index f19a3df42a8..b357b463af5 100644 --- a/includes/api/class-wc-rest-product-attributes-controller.php +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -187,8 +187,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { */ public function get_items( $request ) { $attributes = wc_get_attribute_taxonomies(); - - $data = array(); + $data = array(); foreach ( $attributes as $attribute_obj ) { $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); $attribute = $this->prepare_response_for_collection( $attribute ); @@ -475,8 +474,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { * @return array Links for the given attribute. */ protected function prepare_links( $attribute ) { - $base = '/' . $this->namespace . '/' . $this->rest_base; - + $base = '/' . $this->namespace . '/' . $this->rest_base; $links = array( 'self' => array( 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php index d6301850815..0fe1a27e3ee 100644 --- a/includes/api/class-wc-rest-product-reviews-controller.php +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -107,8 +107,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { } $reviews = get_approved_comments( $product->ID ); - - $data = array(); + $data = array(); foreach ( $reviews as $review_data ) { $review = $this->prepare_item_for_response( $review_data, $request ); $review = $this->prepare_response_for_collection( $review ); @@ -191,8 +190,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { protected function prepare_links( $review, $request ) { $product_id = (int) $request['product_id']; $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); - - $links = array( + $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), ), diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 69a1a66d5dc..249eeb0ba60 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -503,8 +503,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { */ public function prepare_item_for_response( $post, $request ) { $product = wc_get_product( $post ); - - $data = $this->get_product_data( $product ); + $data = $this->get_product_data( $product ); // Add variations to variable products. if ( $product->is_type( 'variable' ) && $product->has_child() ) { diff --git a/includes/api/class-wc-rest-webhook-deliveries.php b/includes/api/class-wc-rest-webhook-deliveries.php index 96facb7079d..55948b7d726 100644 --- a/includes/api/class-wc-rest-webhook-deliveries.php +++ b/includes/api/class-wc-rest-webhook-deliveries.php @@ -188,8 +188,7 @@ class WC_REST_Webhook_Deliveries_Controller extends WP_REST_Controller { protected function prepare_links( $log ) { $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; $base = str_replace( '(?P[\d]+)', $webhook_id, $this->rest_base ); - - $links = array( + $links = array( 'self' => array( 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), ), diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index 9c59a7091bb..229eb15d47c 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -387,8 +387,7 @@ class WC_REST_Webhooks_Controller extends WC_REST_Posts_Controller { public function prepare_item_for_response( $post, $request ) { $id = (int) $post->ID; $webhook = new WC_Webhook( $id ); - - $data = array( + $data = array( 'id' => $webhook->id, 'name' => $webhook->get_name(), 'status' => $webhook->get_status(), From c890961f7623ae260db9c25921b917cb0de01dc3 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:17:39 -0300 Subject: [PATCH 148/177] Fixed contexts on abstract terms controller --- includes/abstracts/abstract-wc-rest-terms-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 3ca2010256f..589098c60da 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -354,7 +354,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { */ do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); @@ -459,7 +459,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { */ do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); return rest_ensure_response( $response ); } @@ -486,7 +486,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { } $term = get_term( (int) $request['id'], $taxonomy ); - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $term, $request ); $retval = wp_delete_term( $term->term_id, $term->taxonomy ); From ab449a6a6293614a1dd3c12ba744e883cac91728 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:32:12 -0300 Subject: [PATCH 149/177] Improved coupons endpoint using the new Coupon Data fields --- .../api/class-wc-rest-coupons-controller.php | 107 ++++++++++-------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 3bce1f415d1..580518964ee 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -141,26 +141,27 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { $data = array( 'id' => $coupon->id, 'code' => $coupon->code, - 'type' => $coupon->type, 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'discount_type' => $coupon->type, + 'description' => $post->post_excerpt, 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), + 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_prepare_date_response( $coupon->expiry_date ) : null, + 'usage_count' => (int) $coupon->usage_count, 'individual_use' => ( 'yes' === $coupon->individual_use ), 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, - 'usage_count' => (int) $coupon->usage_count, - 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? wc_rest_prepare_date_response( $coupon->expiry_date ) : null, - 'enable_free_shipping' => $coupon->enable_free_shipping(), - 'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ), - 'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ), + 'free_shipping' => $coupon->enable_free_shipping(), + 'product_categories' => array_map( 'absint', (array) $coupon->product_categories ), + 'excluded_product_categories' => array_map( 'absint', (array) $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items(), 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), - 'customer_emails' => $coupon->customer_email, - 'description' => $post->post_excerpt, + 'email_restrictions' => $coupon->customer_email, + 'used_by' => $coupon->get_used_by(), ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -290,7 +291,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { $data = $request->get_json_params(); $defaults = array( - 'type' => 'fixed_cart', + 'discount_type' => 'fixed_cart', 'amount' => 0, 'individual_use' => false, 'product_ids' => array(), @@ -300,20 +301,20 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'limit_usage_to_x_items' => '', 'usage_count' => '', 'expiry_date' => '', - 'enable_free_shipping' => false, - 'product_category_ids' => array(), - 'exclude_product_category_ids' => array(), + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', - 'customer_emails' => array(), + 'email_restrictions' => array(), 'description' => '' ); $data = wp_parse_args( $data, $defaults ); // Set coupon meta. - update_post_meta( $post->ID, 'discount_type', $data['type'] ); + update_post_meta( $post->ID, 'discount_type', $data['discount_type'] ); update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); @@ -323,13 +324,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); - update_post_meta( $post->ID, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); - update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); - update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); + update_post_meta( $post->ID, 'free_shipping', ( true === $data['free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_categories'] ) ) ); + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['excluded_product_categories'] ) ) ); update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); - update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['email_restrictions'] ) ) ); return true; } @@ -378,16 +379,16 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); } - if ( isset( $request['enable_free_shipping'] ) ) { - update_post_meta( $post->ID, 'free_shipping', ( true === $request['enable_free_shipping'] ) ? 'yes' : 'no' ); + if ( isset( $request['free_shipping'] ) ) { + update_post_meta( $post->ID, 'free_shipping', ( true === $request['free_shipping'] ) ? 'yes' : 'no' ); } - if ( isset( $request['product_category_ids'] ) ) { - update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_category_ids'] ) ) ); + if ( isset( $request['product_categories'] ) ) { + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_categories'] ) ) ); } - if ( isset( $request['exclude_product_category_ids'] ) ) { - update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['exclude_product_category_ids'] ) ) ); + if ( isset( $request['excluded_product_categories'] ) ) { + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['excluded_product_categories'] ) ) ); } if ( isset( $request['exclude_sale_items'] ) ) { @@ -402,8 +403,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); } - if ( isset( $request['customer_emails'] ) ) { - update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['customer_emails'] ) ) ); + if ( isset( $request['email_restrictions'] ) ) { + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['email_restrictions'] ) ) ); } return true; @@ -432,12 +433,6 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'type' => array( - 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), - 'type' => 'string', - 'enum' => array_keys( wc_get_coupon_types() ), - 'context' => array( 'view', 'edit' ), - ), 'date_created' => array( 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', @@ -450,11 +445,33 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), 'amount' => array( 'description' => __( 'The amount of discount.', 'woocommerce' ), 'type' => 'float', 'context' => array( 'view', 'edit' ), ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), 'individual_use' => array( 'description' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), 'type' => 'boolean', @@ -485,28 +502,17 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'usage_count' => array( - 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'expiry_date' => array( - 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'enable_free_shipping' => array( + 'free_shipping' => array( 'description' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), ), - 'product_category_ids' => array( + 'product_categories' => array( 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), ), - 'exclude_product_category_ids' => array( + 'excluded_product_categories' => array( 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), @@ -526,15 +532,16 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'type' => 'float', 'context' => array( 'view', 'edit' ), ), - 'customer_emails' => array( + 'email_restrictions' => array( 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), ), - 'description' => array( - 'description' => __( 'Coupon description.', 'woocommerce' ), - 'type' => 'string', + 'used_by' => array( + 'description' => __( 'List of user IDs who have used the coupon.', 'woocommerce' ), + 'type' => 'array', 'context' => array( 'view', 'edit' ), + 'readonly' => true, ), ), ); From 5091b36d419f20df190e694bb1cb40c65e050a37 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:34:16 -0300 Subject: [PATCH 150/177] Align variables --- includes/abstracts/abstract-wc-rest-posts-controller.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 1114cf68956..bb3a96f7497 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -159,7 +159,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } $post->post_type = $this->post_type; - $post_id = wp_insert_post( $post, true ); + $post_id = wp_insert_post( $post, true ); if ( is_wp_error( $post_id ) ) { @@ -171,10 +171,9 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $post_id; } $post->ID = $post_id; + $schema = $this->get_item_schema(); + $post = get_post( $post_id ); - $schema = $this->get_item_schema(); - - $post = get_post( $post_id ); $this->update_additional_fields_for_object( $post, $request ); // Add meta fields. From 1021bfb7543168ada1582ec67195cecf618107a9 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 31 Mar 2016 16:57:55 -0300 Subject: [PATCH 151/177] Simplified the way how check terms permissions --- .../abstract-wc-rest-terms-controller.php | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-terms-controller.php b/includes/abstracts/abstract-wc-rest-terms-controller.php index 589098c60da..4b7ec89d9b4 100644 --- a/includes/abstracts/abstract-wc-rest-terms-controller.php +++ b/includes/abstracts/abstract-wc-rest-terms-controller.php @@ -89,12 +89,12 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; } - if ( ! wc_rest_check_product_term_permissions( $taxonomy, 'read' ) ) { + if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -108,12 +108,12 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $permissions = $this->check_permissions( $request, 'create' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; } - if ( ! wc_rest_check_product_term_permissions( $taxonomy, 'create' ) ) { + if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -127,13 +127,12 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; } - $term = get_term( (int) $request['id'], $taxonomy ); - if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'read', $term->term_id ) ) { + if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -147,13 +146,12 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - if ( ! $taxonomy ) { - return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + $permissions = $this->check_permissions( $request, 'edit' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; } - $term = get_term( (int) $request['id'], $taxonomy ); - if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'edit', $term->term_id ) ) { + if ( ! $permissions ) { return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -167,17 +165,44 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'delete' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @param string $context Request context. + * @return bool|WP_Error + */ + protected function check_permissions( $request, $context = 'read' ) { + // Get taxonomy. $taxonomy = $this->get_taxonomy( $request ); if ( ! $taxonomy ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( "Taxonomy doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); } - $term = get_term( (int) $request['id'], $taxonomy ); - if ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, 'delete', $term->term_id ) ) { - return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + // Check permissions for a single term. + if ( $id = intval( $request['id'] ) ) { + $term = get_term( $id, $taxonomy ); + + if ( ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); } - return true; + return wc_rest_check_product_term_permissions( $taxonomy, $context ); } /** @@ -379,10 +404,6 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { $taxonomy = $this->get_taxonomy( $request ); $term = get_term( (int) $request['id'], $taxonomy ); - if ( ! $term || $term->taxonomy !== $taxonomy ) { - return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - if ( is_wp_error( $term ) ) { return $term; } @@ -399,15 +420,11 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { * @return WP_REST_Request|WP_Error */ public function update_item( $request ) { - $taxonomy = $this->get_taxonomy( $request ); - $term = get_term( (int) $request['id'], $taxonomy ); - - if ( ! $term || $term->taxonomy !== $taxonomy ) { - return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); $schema = $this->get_item_schema(); $prepared_args = array(); + if ( isset( $request['name'] ) ) { $prepared_args['name'] = $request['name']; } @@ -473,12 +490,7 @@ abstract class WC_REST_Terms_Controller extends WP_REST_Controller { public function delete_item( $request ) { $taxonomy = $this->get_taxonomy( $request ); $term = get_term( (int) $request['id'], $taxonomy ); - - if ( ! $term || $term->taxonomy !== $taxonomy ) { - return new WP_Error( 'woocommerce_rest_term_invalid', __( "Resource doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); - } - - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; // We don't support trashing for this type, error out. if ( ! $force ) { From c0ec85cd847fc2cc28c65912b07a0459edbdb14d Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 1 Apr 2016 17:30:04 +0100 Subject: [PATCH 152/177] Implement new gateway screen in wizard and auto-install plugin-base gateways when chosen. --- assets/css/activation.css | 2 +- assets/css/activation.scss | 9 +- assets/css/wc-setup.css | 2 +- assets/css/wc-setup.scss | 105 ++++++ assets/images/paypal-braintree.png | Bin 0 -> 17640 bytes assets/images/paypal-express.png | Bin 0 -> 17037 bytes assets/images/stripe.png | Bin 0 -> 4626 bytes assets/js/admin/wc-setup.js | 17 + assets/js/admin/wc-setup.min.js | 2 +- includes/admin/class-wc-admin-notices.php | 45 ++- .../admin/class-wc-admin-setup-wizard.php | 318 ++++++++++++++---- includes/admin/views/html-notice-custom.php | 14 + 12 files changed, 443 insertions(+), 71 deletions(-) create mode 100644 assets/images/paypal-braintree.png create mode 100644 assets/images/paypal-express.png create mode 100644 assets/images/stripe.png create mode 100644 includes/admin/views/html-notice-custom.php diff --git a/assets/css/activation.css b/assets/css/activation.css index c9d37a07f99..87e8fafc2b0 100644 --- a/assets/css/activation.css +++ b/assets/css/activation.css @@ -1 +1 @@ -.woocommerce-message{overflow:hidden;position:relative;border-left-color:#cc99c2!important}.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary{background:#cc99c2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);border-color:#b366a4;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);color:#fff;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.woocommerce-message a.button-primary:hover,p.woocommerce-actions a.button-primary:hover{background:#bb77ae;border-color:#aa559a;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15)}.woocommerce-message a.button-primary:active,p.woocommerce-actions a.button-primary:active{background:#aa559a;border-color:#aa559a}.woocommerce-message a.docs,.woocommerce-message a.skip,p.woocommerce-actions a.docs,p.woocommerce-actions a.skip{opacity:.5}.woocommerce-message a.docs:focus,.woocommerce-message a.docs:hover,.woocommerce-message a.skip:focus,.woocommerce-message a.skip:hover,p.woocommerce-actions a.docs:focus,p.woocommerce-actions a.docs:hover,p.woocommerce-actions a.skip:focus,p.woocommerce-actions a.skip:hover{opacity:1}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:absolute;top:10px;right:10px;padding:10px 15px 10px 21px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close:before,p.woocommerce-actions a.woocommerce-message-close:before{position:absolute;top:8px;left:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message a.button-primary,.woocommerce-message a.button-secondary,p.woocommerce-actions a.button-primary,p.woocommerce-actions a.button-secondary{text-decoration:none!important}.woocommerce-message .twitter-share-button,p.woocommerce-actions .twitter-share-button{margin-top:-3px;margin-left:3px;vertical-align:middle}.woocommerce-about-text,p.woocommerce-actions{margin-bottom:1em!important}div.woocommerce-legacy-shipping-notice,div.woocommerce-no-shipping-methods-notice{overflow:hidden;padding:1px 12px}div.woocommerce-legacy-shipping-notice p,div.woocommerce-no-shipping-methods-notice p{position:relative;z-index:1;max-width:700px;line-height:1.5em;margin:12px 0}div.woocommerce-legacy-shipping-notice p.main,div.woocommerce-no-shipping-methods-notice p.main{font-size:1.1em}div.woocommerce-legacy-shipping-notice:before,div.woocommerce-no-shipping-methods-notice:before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#F7F1F6;display:block;width:1em;font-size:20em;top:36px;right:12px;position:absolute} \ No newline at end of file +div.woocommerce-message{overflow:hidden;position:relative;border-left-color:#cc99c2!important}div.woocommerce-message p{max-width:700px}.woocommerce-message a.button-primary,p.woocommerce-actions a.button-primary{background:#cc99c2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);border-color:#b366a4;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);color:#fff;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.woocommerce-message a.button-primary:hover,p.woocommerce-actions a.button-primary:hover{background:#bb77ae;border-color:#aa559a;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 rgba(0,0,0,.15)}.woocommerce-message a.button-primary:active,p.woocommerce-actions a.button-primary:active{background:#aa559a;border-color:#aa559a}.woocommerce-message a.docs,.woocommerce-message a.skip,p.woocommerce-actions a.docs,p.woocommerce-actions a.skip{opacity:.5}.woocommerce-message a.docs:focus,.woocommerce-message a.docs:hover,.woocommerce-message a.skip:focus,.woocommerce-message a.skip:hover,p.woocommerce-actions a.docs:focus,p.woocommerce-actions a.docs:hover,p.woocommerce-actions a.skip:focus,p.woocommerce-actions a.skip:hover{opacity:1}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:absolute;top:0;right:0;padding:10px 15px 10px 21px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close:before,p.woocommerce-actions a.woocommerce-message-close:before{position:absolute;top:8px;left:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message a.button-primary,.woocommerce-message a.button-secondary,p.woocommerce-actions a.button-primary,p.woocommerce-actions a.button-secondary{text-decoration:none!important}.woocommerce-message .twitter-share-button,p.woocommerce-actions .twitter-share-button{margin-top:-3px;margin-left:3px;vertical-align:middle}.woocommerce-about-text,p.woocommerce-actions{margin-bottom:1em!important}div.woocommerce-legacy-shipping-notice,div.woocommerce-no-shipping-methods-notice{overflow:hidden;padding:1px 12px}div.woocommerce-legacy-shipping-notice p,div.woocommerce-no-shipping-methods-notice p{position:relative;z-index:1;max-width:700px;line-height:1.5em;margin:12px 0}div.woocommerce-legacy-shipping-notice p.main,div.woocommerce-no-shipping-methods-notice p.main{font-size:1.1em}div.woocommerce-legacy-shipping-notice:before,div.woocommerce-no-shipping-methods-notice:before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#F7F1F6;display:block;width:1em;font-size:20em;top:36px;right:12px;position:absolute} \ No newline at end of file diff --git a/assets/css/activation.scss b/assets/css/activation.scss index 7d5cca4eeb2..d1b3b53930c 100644 --- a/assets/css/activation.scss +++ b/assets/css/activation.scss @@ -11,10 +11,13 @@ /** * Styling begins */ -.woocommerce-message { +div.woocommerce-message { overflow: hidden; position: relative; border-left-color: #cc99c2 !important; + p { + max-width: 700px; + } } p.woocommerce-actions, @@ -51,8 +54,8 @@ p.woocommerce-actions, a.woocommerce-message-close { position: absolute; - top: 10px; - right: 10px; + top: 0; + right: 0; padding: 10px 15px 10px 21px; font-size: 13px; line-height: 1.23076923; diff --git a/assets/css/wc-setup.css b/assets/css/wc-setup.css index 52462a3210d..ff37a0edb8c 100644 --- a/assets/css/wc-setup.css +++ b/assets/css/wc-setup.css @@ -1 +1 @@ -.wc-setup-content p,.wc-setup-content table{font-size:1em;line-height:1.75em;color:#666}body{margin:100px auto 24px;box-shadow:none;background:#f1f1f1;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-setup-content{box-shadow:0 1px 3px rgba(0,0,0,.13);padding:24px 24px 0;background:#fff;overflow:hidden;zoom:1}.wc-setup-content h1,.wc-setup-content h2,.wc-setup-content h3,.wc-setup-content table{margin:0 0 24px;border:0;padding:0;color:#666;clear:none}.wc-setup-content p{margin:0 0 24px}.wc-setup-content a{color:#a16696}.wc-setup-content a:focus,.wc-setup-content a:hover{color:#111}.wc-setup-content .form-table th{width:35%;vertical-align:top;font-weight:400}.wc-setup-content .form-table td{vertical-align:top}.wc-setup-content .form-table td input,.wc-setup-content .form-table td select{width:100%;box-sizing:border-box}.wc-setup-content .form-table td input[size]{width:auto}.wc-setup-content .form-table td .description{line-height:1.5em;display:block;margin-top:.25em;color:#999;font-style:italic}.wc-setup-content .form-table td .input-checkbox,.wc-setup-content .form-table td .input-radio{width:auto;box-sizing:inherit;padding:inherit;margin:0 .5em 0 0;box-shadow:none}.wc-setup-content .form-table .section_title td{padding:0}.wc-setup-content .form-table .section_title td h2,.wc-setup-content .form-table .section_title td p{margin:12px 0 0}.wc-setup-content .form-table td,.wc-setup-content .form-table th{padding:12px 0;margin:0;border:0}.wc-setup-content .form-table td:first-child,.wc-setup-content .form-table th:first-child{padding-right:1em}.wc-setup-content table.tax-rates{width:100%;font-size:.92em}.wc-setup-content table.tax-rates th{padding:0;text-align:center;width:auto;vertical-align:middle}.wc-setup-content table.tax-rates td{border:1px solid #f5f5f5;padding:6px;text-align:center;vertical-align:middle}.wc-setup-content table.tax-rates td input{outline:0;border:0;padding:0;box-shadow:none;text-align:center;width:100%}.wc-setup-content table.tax-rates .add,.wc-setup-content table.tax-rates .remove{padding:1em 0 0 1em;line-height:1em;width:0;height:0;display:inline-block;font-size:1em;overflow:hidden}.wc-setup-content table.tax-rates td.sort{cursor:move;color:#ccc}.wc-setup-content table.tax-rates td.sort:before{content:"\f333";font-family:dashicons}.wc-setup-content table.tax-rates td.readonly{background:#f5f5f5}.wc-setup-content table.tax-rates .add{margin:6px 0 0;position:relative}.wc-setup-content table.tax-rates .add:before{content:"\f502";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content table.tax-rates .remove{margin:0;position:relative}.wc-setup-content table.tax-rates .remove:before{content:"\f182";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content .wc-setup-pages{width:100%;border-top:1px solid #eee}.wc-setup-content .wc-setup-pages thead th{display:none}.wc-setup-content .wc-setup-pages .page-name{width:30%;font-weight:700}.wc-setup-content .wc-setup-pages td,.wc-setup-content .wc-setup-pages th{padding:14px 0;border-bottom:1px solid #eee}.wc-setup-content .wc-setup-pages td:first-child,.wc-setup-content .wc-setup-pages th:first-child{padding-right:9px}.wc-setup-content .wc-setup-pages th{padding-top:0}.wc-setup-content .wc-setup-pages .page-options p{color:#777;margin:6px 0 0 24px;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p input{vertical-align:middle;margin:1px 0 0;height:1.75em;width:1.75em;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p label{line-height:1}@media screen and (max-width:782px){.wc-setup-content .form-table tbody th{width:auto}}.wc-setup-content .twitter-share-button{float:right}.wc-setup-content .wc-setup-next-steps{overflow:hidden;margin:0 0 24px}.wc-setup-content .wc-setup-next-steps h2{margin-bottom:12px}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-first{float:left;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-last{float:right;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps ul{padding:0 2em 0 0;list-style:none;margin:0 0 -.75em}.wc-setup-content .wc-setup-next-steps ul li a{display:block;padding:0 0 .75em}.wc-setup-content .wc-setup-next-steps ul .setup-product a{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);font-size:1em;height:auto;line-height:1.75em;margin:0 0 .75em;opacity:1;padding:1em;text-align:center;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-setup-content .wc-setup-next-steps ul li a:before{color:#82878c;font:400 20px/1 dashicons;speak:none;display:inline-block;padding:0 10px 0 0;top:1px;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important;vertical-align:top}.wc-setup-content .wc-setup-next-steps ul .learn-more a:before{content:"\f105"}.wc-setup-content .wc-setup-next-steps ul .video-walkthrough a:before{content:"\f126"}.wc-setup-content .wc-setup-next-steps ul .sidekick a:before{content:"\f118"}.wc-setup-content .wc-setup-next-steps ul .newsletter a:before{content:"\f465"}.wc-setup-content .updated,.wc-setup-content .woocommerce-tracker{padding:24px 24px 0;margin:0 0 24px;overflow:hidden;background:#f5f5f5}.wc-setup-content .updated p,.wc-setup-content .woocommerce-tracker p{padding:0;margin:0 0 12px}.wc-setup-content .updated p:last-child,.wc-setup-content .woocommerce-tracker p:last-child{margin:0 0 24px}.wc-setup-steps{padding:0 0 24px;margin:0;list-style:none;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex}.wc-setup-steps li{width:20%;float:left;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.wc-setup-steps li:before{content:"";border:4px solid #ccc;border-radius:100%;width:4px;height:4px;position:absolute;bottom:0;left:50%;margin-left:-6px;margin-bottom:-8px;background:#fff}.wc-setup-steps li.active{border-color:#a16696;color:#a16696}.wc-setup-steps li.active:before{border-color:#a16696}.wc-setup-steps li.done{border-color:#a16696;color:#a16696}.wc-setup-steps li.done:before{border-color:#a16696;background:#a16696}.wc-setup .wc-setup-actions{overflow:hidden}.wc-setup .wc-setup-actions .button{float:right;font-size:1.25em;padding:.5em 1em;line-height:1em;margin-right:.5em;height:auto}.wc-setup .wc-setup-actions .button-primary{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);float:right;margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-return-to-dashboard{font-size:.85em;color:#b5b5b5;margin:1.18em 0;display:block;text-align:center} \ No newline at end of file +.wc-setup-content p,.wc-setup-content table{font-size:1em;line-height:1.75em;color:#666}body{margin:100px auto 24px;box-shadow:none;background:#f1f1f1;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-setup-content{box-shadow:0 1px 3px rgba(0,0,0,.13);padding:24px 24px 0;background:#fff;overflow:hidden;zoom:1}.wc-setup-content h1,.wc-setup-content h2,.wc-setup-content h3,.wc-setup-content table{margin:0 0 24px;border:0;padding:0;color:#666;clear:none}.wc-setup-content p{margin:0 0 24px}.wc-setup-content a{color:#a16696}.wc-setup-content a:focus,.wc-setup-content a:hover{color:#111}.wc-setup-content .form-table th{width:35%;vertical-align:top;font-weight:400}.wc-setup-content .form-table td{vertical-align:top}.wc-setup-content .form-table td input,.wc-setup-content .form-table td select{width:100%;box-sizing:border-box}.wc-setup-content .form-table td input[size]{width:auto}.wc-setup-content .form-table td .description{line-height:1.5em;display:block;margin-top:.25em;color:#999;font-style:italic}.wc-setup-content .form-table td .input-checkbox,.wc-setup-content .form-table td .input-radio{width:auto;box-sizing:inherit;padding:inherit;margin:0 .5em 0 0;box-shadow:none}.wc-setup-content .form-table .section_title td{padding:0}.wc-setup-content .form-table .section_title td h2,.wc-setup-content .form-table .section_title td p{margin:12px 0 0}.wc-setup-content .form-table td,.wc-setup-content .form-table th{padding:12px 0;margin:0;border:0}.wc-setup-content .form-table td:first-child,.wc-setup-content .form-table th:first-child{padding-right:1em}.wc-setup-content table.tax-rates{width:100%;font-size:.92em}.wc-setup-content table.tax-rates th{padding:0;text-align:center;width:auto;vertical-align:middle}.wc-setup-content table.tax-rates td{border:1px solid #f5f5f5;padding:6px;text-align:center;vertical-align:middle}.wc-setup-content table.tax-rates td input{outline:0;border:0;padding:0;box-shadow:none;text-align:center;width:100%}.wc-setup-content table.tax-rates .add,.wc-setup-content table.tax-rates .remove{padding:1em 0 0 1em;line-height:1em;width:0;height:0;display:inline-block;font-size:1em;overflow:hidden}.wc-setup-content table.tax-rates td.sort{cursor:move;color:#ccc}.wc-setup-content table.tax-rates td.sort:before{content:"\f333";font-family:dashicons}.wc-setup-content table.tax-rates td.readonly{background:#f5f5f5}.wc-setup-content table.tax-rates .add{margin:6px 0 0;position:relative}.wc-setup-content table.tax-rates .add:before{content:"\f502";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content table.tax-rates .remove{margin:0;position:relative}.wc-setup-content table.tax-rates .remove:before{content:"\f182";font-family:dashicons;position:absolute;left:0;top:0}.wc-setup-content .wc-setup-pages{width:100%;border-top:1px solid #eee}.wc-setup-content .wc-setup-pages thead th{display:none}.wc-setup-content .wc-setup-pages .page-name{width:30%;font-weight:700}.wc-setup-content .wc-setup-pages td,.wc-setup-content .wc-setup-pages th{padding:14px 0;border-bottom:1px solid #eee}.wc-setup-content .wc-setup-pages td:first-child,.wc-setup-content .wc-setup-pages th:first-child{padding-right:9px}.wc-setup-content .wc-setup-pages th{padding-top:0}.wc-setup-content .wc-setup-pages .page-options p{color:#777;margin:6px 0 0 24px;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p input{vertical-align:middle;margin:1px 0 0;height:1.75em;width:1.75em;line-height:1.75em}.wc-setup-content .wc-setup-pages .page-options p label{line-height:1}@media screen and (max-width:782px){.wc-setup-content .form-table tbody th{width:auto}}.wc-setup-content .twitter-share-button{float:right}.wc-setup-content .wc-setup-next-steps{overflow:hidden;margin:0 0 24px}.wc-setup-content .wc-setup-next-steps h2{margin-bottom:12px}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-first{float:left;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps .wc-setup-next-steps-last{float:right;width:50%;box-sizing:border-box}.wc-setup-content .wc-setup-next-steps ul{padding:0 2em 0 0;list-style:none;margin:0 0 -.75em}.wc-setup-content .wc-setup-next-steps ul li a{display:block;padding:0 0 .75em}.wc-setup-content .wc-setup-next-steps ul .setup-product a{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);font-size:1em;height:auto;line-height:1.75em;margin:0 0 .75em;opacity:1;padding:1em;text-align:center;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-setup-content .wc-setup-next-steps ul li a:before{color:#82878c;font:400 20px/1 dashicons;speak:none;display:inline-block;padding:0 10px 0 0;top:1px;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none!important;vertical-align:top}.wc-setup-content .wc-setup-next-steps ul .learn-more a:before{content:"\f105"}.wc-setup-content .wc-setup-next-steps ul .video-walkthrough a:before{content:"\f126"}.wc-setup-content .wc-setup-next-steps ul .sidekick a:before{content:"\f118"}.wc-setup-content .wc-setup-next-steps ul .newsletter a:before{content:"\f465"}.wc-setup-content .updated,.wc-setup-content .woocommerce-tracker{padding:24px 24px 0;margin:0 0 24px;overflow:hidden;background:#f5f5f5}.wc-setup-content .updated p,.wc-setup-content .woocommerce-tracker p{padding:0;margin:0 0 12px}.wc-setup-content .updated p:last-child,.wc-setup-content .woocommerce-tracker p:last-child{margin:0 0 24px}.wc-setup-steps{padding:0 0 24px;margin:0;list-style:none;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex}.wc-setup-steps li{width:20%;float:left;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.wc-setup-steps li:before{content:"";border:4px solid #ccc;border-radius:100%;width:4px;height:4px;position:absolute;bottom:0;left:50%;margin-left:-6px;margin-bottom:-8px;background:#fff}.wc-setup-steps li.active{border-color:#a16696;color:#a16696}.wc-setup-steps li.active:before{border-color:#a16696}.wc-setup-steps li.done{border-color:#a16696;color:#a16696}.wc-setup-steps li.done:before{border-color:#a16696;background:#a16696}.wc-setup .wc-setup-actions{overflow:hidden}.wc-setup .wc-setup-actions .button{float:right;font-size:1.25em;padding:.5em 1em;line-height:1em;margin-right:.5em;height:auto}.wc-setup .wc-setup-actions .button-primary{background-color:#a16696;border-color:#a16696;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);float:right;margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-return-to-dashboard{font-size:.85em;color:#b5b5b5;margin:1.18em 0;display:block;text-align:center}ul.wc-wizard-payment-gateways{border:1px solid #eee;border-bottom-width:2px;padding:0;margin:0 0 1em;list-style:none;border-radius:4px}ul.wc-wizard-payment-gateways li.wc-wizard-gateway{padding:1.5em 1em 1em 4em;border-top:1px solid #eee;list-style:none;cursor:pointer;color:#555;box-sizing:border-box;clear:both}ul.wc-wizard-payment-gateways li.wc-wizard-gateway:first-child{border-top:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.featured{width:50%;float:left;clear:none;border-right:1px solid #eee;border-top:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.featured-row-last{border-right:0}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-settings{display:none;margin-bottom:0;cursor:default}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.checked{background:#F7F1F6}ul.wc-wizard-payment-gateways li.wc-wizard-gateway.checked .wc-wizard-gateway-settings{display:table}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-description{font-style:italic}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-description p,ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable{margin:0 0 .5em}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label{display:block;font-weight:700;vertical-align:middle}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label img{max-height:2em;vertical-align:middle}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable label:before{content:'';background:#fff;border:2px solid #eee;display:inline-block;float:left;vertical-align:middle;width:2em;height:2em;padding:0;box-sizing:border-box;line-height:1.8em;text-align:center;margin:0 1em 0 -3em;border-radius:4px}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input{opacity:0;width:0;position:absolute}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input:checked+label:before{content:"\f147";font-family:dashicons;color:#fff;border-color:#a46497;background:#a46497}ul.wc-wizard-payment-gateways li.wc-wizard-gateway .wc-wizard-gateway-enable input:focus+label{outline:#ddd solid 1px} \ No newline at end of file diff --git a/assets/css/wc-setup.scss b/assets/css/wc-setup.scss index 15577d49a8a..50944542d4d 100644 --- a/assets/css/wc-setup.scss +++ b/assets/css/wc-setup.scss @@ -370,3 +370,108 @@ body { display: block; text-align: center; } + +ul.wc-wizard-payment-gateways { + border: 1px solid #eee; + border-bottom-width: 2px; + padding: 0; + margin: 0 0 1em; + list-style: none outside; + border-radius: 4px; + + li.wc-wizard-gateway { + padding: 1.5em 1em 1em 4em; + border-top: 1px solid #eee; + list-style: none outside; + cursor: pointer; + color: #555; + box-sizing: border-box; + clear: both; + + &:first-child { + border-top: 0; + } + + &.featured { + width: 50%; + float: left; + clear: none; + border-right: 1px solid #eee; + border-top: 0; + } + + &.featured-row-last { + border-right: 0; + } + + .wc-wizard-gateway-settings { + display: none; + margin-bottom: 0; + cursor: default; + } + + &.checked { + background: #F7F1F6; + + .wc-wizard-gateway-settings { + display: table; + } + } + + .wc-wizard-gateway-description { + font-style: italic; + + p { + margin: 0 0 .5em; + } + } + + .wc-wizard-gateway-enable { + margin: 0 0 .5em; + + label { + display: block; + font-weight: bold; + vertical-align: middle; + + img { + max-height: 2em; + vertical-align: middle; + } + } + label:before { + content: ''; + background: #fff; + border: 2px solid #eee; + display: inline-block; + float: left; + vertical-align: middle; + width: 2em; + height: 2em; + padding: 0; + box-sizing: border-box; + line-height: 1.8em; + text-align: center; + margin: 0 1em 0 -3em; + border-radius: 4px; + } + input { + opacity: 0; + width: 0; + position: absolute; + + &:checked + label:before { + content: "\f147"; + font-family: 'dashicons'; + color: #fff; + border-color: #a46497; + background: #a46497; + } + + &:focus + label { + outline: 1px solid #ddd; /* focus style */ + } + } + } + } +} diff --git a/assets/images/paypal-braintree.png b/assets/images/paypal-braintree.png new file mode 100644 index 0000000000000000000000000000000000000000..92c3bb5f2c7a7ae1fac8208d6ac0650bf75e1296 GIT binary patch literal 17640 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 literal 0 HcmV?d00001 diff --git a/assets/images/stripe.png b/assets/images/stripe.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0ad7a535aa0edb8064d3511d1acaced08dfa64 GIT binary patch literal 4626 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 literal 0 HcmV?d00001 diff --git a/assets/js/admin/wc-setup.js b/assets/js/admin/wc-setup.js index 9ab675215de..96145eaac7f 100644 --- a/assets/js/admin/wc-setup.js +++ b/assets/js/admin/wc-setup.js @@ -46,4 +46,21 @@ jQuery( function( $ ) { return true; } ); + $( '.wc-wizard-payment-gateways' ).on( 'change', '.wc-wizard-gateway-enable input', function() { + if ( $( this ).is( ':checked' ) ) { + $( this ).closest( 'li' ).addClass( 'checked' ); + } else { + $( this ).closest( 'li' ).removeClass( 'checked' ); + } + } ); + + $( '.wc-wizard-payment-gateways' ).on( 'click', 'li.wc-wizard-gateway', function() { + var $enabled = $( this ).find( '.wc-wizard-gateway-enable input' ); + + $enabled.prop( 'checked', ! $enabled.prop( 'checked' ) ).change(); + } ); + + $( '.wc-wizard-payment-gateways' ).on( 'click', 'li.wc-wizard-gateway table, li.wc-wizard-gateway a', function( e ) { + e.stopPropagation(); + } ); } ); diff --git a/assets/js/admin/wc-setup.min.js b/assets/js/admin/wc-setup.min.js index fdb451c1978..5dcaf11b467 100644 --- a/assets/js/admin/wc-setup.min.js +++ b/assets/js/admin/wc-setup.min.js @@ -1 +1 @@ -jQuery(function(a){var b=a.parseJSON(wc_setup_params.locale_info);a('select[name="store_location"]').change(function(){var c=a(this).val(),d=c.split(":")[0],e=b[d],f=["thousand_sep","decimal_sep","num_decimals","currency_pos"];e?a.each(e,function(b,c){a(':input[name="'+b+'"]').val(c).change(),-1!==a.inArray(b,f)&&a(':input[name="'+b+'"]').closest("tr").hide()}):(a(':input[name="currency_pos"]').closest("tr").show(),a(':input[name="thousand_sep"]').closest("tr").show(),a(':input[name="decimal_sep"]').closest("tr").show(),a(':input[name="num_decimals"]').closest("tr").show())}).change(),a('input[name="woocommerce_calc_taxes"]').change(function(){a(this).is(":checked")?(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").show(),a("tr.tax-rates").show()):(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").hide(),a("tr.tax-rates").hide())}).change(),a(".button-next").on("click",function(){return a(".wc-setup-content").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),!0})}); \ No newline at end of file +jQuery(function(a){var b=a.parseJSON(wc_setup_params.locale_info);a('select[name="store_location"]').change(function(){var c=a(this).val(),d=c.split(":")[0],e=b[d],f=["thousand_sep","decimal_sep","num_decimals","currency_pos"];e?a.each(e,function(b,c){a(':input[name="'+b+'"]').val(c).change(),-1!==a.inArray(b,f)&&a(':input[name="'+b+'"]').closest("tr").hide()}):(a(':input[name="currency_pos"]').closest("tr").show(),a(':input[name="thousand_sep"]').closest("tr").show(),a(':input[name="decimal_sep"]').closest("tr").show(),a(':input[name="num_decimals"]').closest("tr").show())}).change(),a('input[name="woocommerce_calc_taxes"]').change(function(){a(this).is(":checked")?(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").show(),a("tr.tax-rates").show()):(a(':input[name="woocommerce_prices_include_tax"], :input[name="woocommerce_import_tax_rates"]').closest("tr").hide(),a("tr.tax-rates").hide())}).change(),a(".button-next").on("click",function(){return a(".wc-setup-content").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),!0}),a(".wc-wizard-payment-gateways").on("change",".wc-wizard-gateway-enable input",function(){a(this).is(":checked")?a(this).closest("li").addClass("checked"):a(this).closest("li").removeClass("checked")}),a(".wc-wizard-payment-gateways").on("click","li.wc-wizard-gateway",function(){var b=a(this).find(".wc-wizard-gateway-enable input");b.prop("checked",!b.prop("checked")).change()}),a(".wc-wizard-payment-gateways").on("click","li.wc-wizard-gateway table, li.wc-wizard-gateway a",function(a){a.stopPropagation()})}); \ No newline at end of file diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index ff154db506e..648b0fd145c 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -22,12 +22,12 @@ class WC_Admin_Notices { * @var array */ private $core_notices = array( - 'install' => 'install_notice', - 'update' => 'update_notice', - 'template_files' => 'template_file_check_notice', - 'theme_support' => 'theme_check_notice', - 'legacy_shipping' => 'legacy_shipping_notice', - 'no_shipping_methods' => 'no_shipping_methods_notice', + 'install' => 'install_notice', + 'update' => 'update_notice', + 'template_files' => 'template_file_check_notice', + 'theme_support' => 'theme_check_notice', + 'legacy_shipping' => 'legacy_shipping_notice', + 'no_shipping_methods' => 'no_shipping_methods_notice', ); /** @@ -62,7 +62,7 @@ class WC_Admin_Notices { /** * Show a notice. - * @param string $name + * @param string $name */ public static function add_notice( $name ) { $notices = array_unique( array_merge( get_option( 'woocommerce_admin_notices', array() ), array( $name ) ) ); @@ -76,6 +76,7 @@ class WC_Admin_Notices { public static function remove_notice( $name ) { $notices = array_diff( get_option( 'woocommerce_admin_notices', array() ), array( $name ) ); update_option( 'woocommerce_admin_notices', $notices ); + delete_option( 'woocommerce_admin_notice_' . $name ); } /** @@ -117,6 +118,36 @@ class WC_Admin_Notices { foreach ( $notices as $notice ) { if ( ! empty( $this->core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { add_action( 'admin_notices', array( $this, $this->core_notices[ $notice ] ) ); + } else { + add_action( 'admin_notices', array( $this, 'output_custom_notices' ) ); + } + } + } + } + + /** + * Add a custom notice. + * @param string $name + * @param string $notice_html + */ + public static function add_custom_notice( $name, $notice_html ) { + self::add_notice( $name ); + update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); + } + + /** + * Output any stored custom notices. + */ + public function output_custom_notices() { + $notices = get_option( 'woocommerce_admin_notices', array() ); + if ( $notices ) { + foreach ( $notices as $notice ) { + if ( empty( $this->core_notices[ $notice ] ) ) { + $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); + + if ( $notice_html ) { + include( 'views/html-notice-custom.php' ); + } } } } diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index a8b3ebab43e..06f5c7a7431 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -557,56 +557,132 @@ class WC_Admin_Setup_Wizard { exit; } + /** + * Simple array of gateways to show in wizard. + * @return array + */ + protected function get_wizard_payment_gateways() { + $gateways = array( + 'stripe' => array( + 'name' => __( 'Stripe', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-first', + 'repo-slug' => 'woocommerce-gateway-stripe', + ), + 'paypal-braintree' => array( + 'name' => __( 'PayPal by Braintree', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal-braintree.png', + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-powered-by-braintree', + ), + 'paypal-ec' => array( + 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/paypal-express.png', + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-last', + 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', + ), + 'paypal' => array( + 'name' => __( 'PayPal Standard', 'woocommerce' ), + 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), + 'image' => '', + 'class' => '', + 'settings' => array( + 'email' => array( + 'label' => __( 'PayPal email address', 'woocommerce' ), + 'type' => 'email', + 'value' => get_option( 'admin_email' ), + 'placeholder' => __( 'PayPal email address', 'woocommerce' ), + ), + ), + ), + 'cheque' => array( + 'name' => __( 'Cheque Payments', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept Cheque payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'bacs' => array( + 'name' => __( 'Bank Transfer (BACS) Payments', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ), + 'cod' => array( + 'name' => __( 'Cash on Delivery', 'woocommerce' ), + 'description' => __( 'An simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), + 'image' => '', + 'class' => '', + ) + ); + + $country = WC()->countries->get_base_country(); + + if ( 'US' === $country ) { + unset( $gateways['paypal-ec'] ); + } else { + unset( $gateways['paypal-braintree'] ); + } + + if ( ! current_user_can( 'install_plugins' ) ) { + unset( $gateways['paypal-braintree'] ); + unset( $gateways['paypal-ec'] ); + unset( $gateways['stripe'] ); + } + + return $gateways; + } + /** * Payments Step. */ public function wc_setup_payments() { - $paypal_settings = array_filter( (array) get_option( 'woocommerce_paypal_settings', array() ) ); - $cheque_settings = array_filter( (array) get_option( 'woocommerce_cheque_settings', array() ) ); - $cod_settings = array_filter( (array) get_option( 'woocommerce_cod_settings', array() ) ); - $bacs_settings = array_filter( (array) get_option( 'woocommerce_bacs_settings', array() ) ); + $gateways = $this->get_wizard_payment_gateways(); ?>

-
+

', '', '' ); ?>

- - - - - - - - - - - - - - - - - - - - - - - -
-

-

-
- -
-

-

-
- -
- -
- -
+ +
    + $gateway ) : ?> +
  • +
    + + +
    +
    + +
    + + + $setting ) : ?> + + + + + +
    + +
    + +
  • + +

@@ -616,31 +692,157 @@ class WC_Admin_Setup_Wizard { get_wizard_payment_gateways(); + $installed_plugins = array_map( array( $this, 'format_plugin_slug' ), array_keys( get_plugins() ) ); - $paypal_settings['enabled'] = ! empty( $_POST['woocommerce_paypal_email'] ) ? 'yes' : 'no'; - $cheque_settings['enabled'] = isset( $_POST['woocommerce_enable_cheque'] ) ? 'yes' : 'no'; - $cod_settings['enabled'] = isset( $_POST['woocommerce_enable_cod'] ) ? 'yes' : 'no'; - $bacs_settings['enabled'] = isset( $_POST['woocommerce_enable_bacs'] ) ? 'yes' : 'no'; + foreach ( $gateways as $gateway_id => $gateway ) { + // If repo-slug is defined, download and install plugin from .org. + if ( ! empty( $gateway['repo-slug'] ) && ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ) { + $plugin_slug = $gateway['repo-slug']; + $plugin = $plugin_slug . '/' . $plugin_slug . '.php'; + $installed = false; + $activate = false; - if ( ! empty( $_POST['woocommerce_paypal_email'] ) ) { - $paypal_settings['email'] = wc_clean( $_POST['woocommerce_paypal_email'] ); + // See if the plugin is installed already + if ( in_array( $gateway['repo-slug'], $installed_plugins ) ) { + $installed = true; + $activate = ! is_plugin_active( $plugin ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback + ob_start(); + + try { + $plugin = plugins_api( 'plugin_information', array( + 'slug' => $gateway['repo-slug'], + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) ); + + if ( is_wp_error( $plugin ) ) { + throw new Exception( $plugin->get_error_message() ); + } + + $package = $plugin->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $gateway_id . '_install_error', + sprintf( + __( '%s could not be installed (%s). %sPlease install it manually by clicking here.%s', 'woocommerce' ), + $gateway['name'], + $e->getMessage(), + '', + '' + ) + ); + } + + // Discard feedback + ob_end_clean(); + } + + // Activate this thing + if ( $activate ) { + try { + $result = activate_plugin( $plugin ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $gateway_id . '_install_error', + sprintf( + __( '%s could not be activated (%s). %sPlease activate it manually via the plugins screen.%s', 'woocommerce' ), + $gateway['name'], + $e->getMessage(), + '', + '' + ) + ); + } + } + } + + $settings_key = 'woocommerce_' . $gateway_id . '_settings'; + $settings = array_filter( (array) get_option( $settings_key, array() ) ); + $settings['enabled'] = ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ? 'yes' : 'no'; + + if ( ! empty( $gateway['settings'] ) ) { + foreach ( $gateway['settings'] as $setting_id => $setting ) { + $settings[ $setting_id ] = wc_clean( $_POST[ $gateway_id . '_' . $setting_id ] ); + } + } + + update_option( $settings_key, $settings ); } - update_option( 'woocommerce_paypal_settings', $paypal_settings ); - update_option( 'woocommerce_cheque_settings', $cheque_settings ); - update_option( 'woocommerce_cod_settings', $cod_settings ); - update_option( 'woocommerce_bacs_settings', $bacs_settings ); - wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); exit; } diff --git a/includes/admin/views/html-notice-custom.php b/includes/admin/views/html-notice-custom.php new file mode 100644 index 00000000000..4a020d7afdf --- /dev/null +++ b/includes/admin/views/html-notice-custom.php @@ -0,0 +1,14 @@ + +

+ + +
From 3c97c9356931669708b4a2db9eec9d236207b794 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 1 Apr 2016 17:33:00 +0100 Subject: [PATCH 153/177] fix spacing --- includes/admin/class-wc-admin-notices.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index 648b0fd145c..d1e64cf04a8 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -22,12 +22,12 @@ class WC_Admin_Notices { * @var array */ private $core_notices = array( - 'install' => 'install_notice', - 'update' => 'update_notice', - 'template_files' => 'template_file_check_notice', - 'theme_support' => 'theme_check_notice', - 'legacy_shipping' => 'legacy_shipping_notice', - 'no_shipping_methods' => 'no_shipping_methods_notice', + 'install' => 'install_notice', + 'update' => 'update_notice', + 'template_files' => 'template_file_check_notice', + 'theme_support' => 'theme_check_notice', + 'legacy_shipping' => 'legacy_shipping_notice', + 'no_shipping_methods' => 'no_shipping_methods_notice', ); /** From cae8849af93ff7029eef34047ea29c0737ef86f3 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 1 Apr 2016 14:36:10 -0300 Subject: [PATCH 154/177] Fixes on product params --- .../api/class-wc-rest-products-controller.php | 129 +++++++++--------- 1 file changed, 65 insertions(+), 64 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 249eeb0ba60..0408792f3af 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -387,11 +387,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', - 'sale_price_dates_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', - 'sale_price_dates_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', + 'date_on_sale_from' => $product->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', + 'date_on_sale_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', 'price_html' => $product->get_price_html(), 'on_sale' => $product->is_on_sale(), - 'purchaseable' => $product->is_purchasable(), + 'purchasable' => $product->is_purchasable(), 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), 'virtual' => $product->is_virtual(), 'downloadable' => $product->is_downloadable(), @@ -403,7 +403,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), - 'managing_stock' => $product->managing_stock(), + 'manage_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders' => $product->backorders, @@ -455,39 +455,39 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } $variations[] = array( - 'id' => $variation->get_variation_id(), - 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price(), - 'sale_price_dates_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', - 'sale_price_dates_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', - 'on_sale' => $variation->is_on_sale(), - 'purchaseable' => $variation->is_purchasable(), - 'virtual' => $variation->is_virtual(), - 'downloadable' => $variation->is_downloadable(), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => (int) $variation->download_limit, - 'download_expiry' => (int) $variation->download_expiry, - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'managing_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders' => $variation->backorders, - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'weight' => $variation->get_weight(), - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => $variation->get_shipping_class_id(), - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), + 'id' => $variation->get_variation_id(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'date_on_sale_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchasable' => $variation->is_purchasable(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $variation->download_limit, + 'download_expiry' => (int) $variation->download_expiry, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'manage_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->backorders, + 'backorders_allowed'=> $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), ); } @@ -1015,15 +1015,15 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $sale_price = get_post_meta( $product->id, '_sale_price', true ); } - if ( isset( $request['sale_price_dates_from'] ) ) { - $date_from = $request['sale_price_dates_from']; + if ( isset( $request['date_on_sale_from'] ) ) { + $date_from = $request['date_on_sale_from']; } else { $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } - if ( isset( $request['sale_price_dates_to'] ) ) { - $date_to = $request['sale_price_dates_to']; + if ( isset( $request['date_on_sale_to'] ) ) { + $date_to = $request['date_on_sale_to']; } else { $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); @@ -1093,11 +1093,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Stock data. if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. - if ( isset( $request['managing_stock'] ) ) { - $managing_stock = ( true === $request['managing_stock'] ) ? 'yes' : 'no'; - update_post_meta( $product->id, '_manage_stock', $managing_stock ); + if ( isset( $request['manage_stock'] ) ) { + $manage_stock = ( true === $request['manage_stock'] ) ? 'yes' : 'no'; + update_post_meta( $product->id, '_manage_stock', $manage_stock ); } else { - $managing_stock = get_post_meta( $product->id, '_manage_stock', true ); + $manage_stock = get_post_meta( $product->id, '_manage_stock', true ); } // Backorders. @@ -1122,7 +1122,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { wc_update_product_stock_status( $product->id, 'instock' ); } elseif ( 'variable' === $product_type ) { update_post_meta( $product->id, '_stock', '' ); - } elseif ( 'yes' == $managing_stock ) { + } elseif ( 'yes' == $manage_stock ) { update_post_meta( $product->id, '_backorders', $backorders ); wc_update_product_stock_status( $product->id, $stock_status ); @@ -1364,13 +1364,13 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $this->save_product_shipping_data( $variation_id, $variation ); // Stock handling. - if ( isset( $variation['managing_stock'] ) ) { - $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; + if ( isset( $variation['manage_stock'] ) ) { + $manage_stock = ( true === $variation['manage_stock'] ) ? 'yes' : 'no'; } else { - $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); + $manage_stock = get_post_meta( $variation_id, '_manage_stock', true ); } - update_post_meta( $variation_id, '_manage_stock', '' === $managing_stock ? 'no' : $managing_stock ); + update_post_meta( $variation_id, '_manage_stock', '' === $manage_stock ? 'no' : $manage_stock ); if ( isset( $variation['in_stock'] ) ) { $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; @@ -1380,7 +1380,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); - if ( 'yes' === $managing_stock ) { + if ( 'yes' === $manage_stock ) { $backorders = get_post_meta( $variation_id, '_backorders', true ); if ( isset( $variation['backorders'] ) ) { @@ -1416,15 +1416,15 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $sale_price = get_post_meta( $variation_id, '_sale_price', true ); } - if ( isset( $variation['sale_price_dates_from'] ) ) { - $date_from = $variation['sale_price_dates_from']; + if ( isset( $variation['date_on_sale_from'] ) ) { + $date_from = $variation['date_on_sale_from']; } else { $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); } - if ( isset( $variation['sale_price_dates_to'] ) ) { - $date_to = $variation['sale_price_dates_to']; + if ( isset( $variation['date_on_sale_to'] ) ) { + $date_to = $variation['date_on_sale_to']; } else { $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); @@ -1770,12 +1770,12 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'sale_price_dates_from' => array( + 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'sale_price_dates_to' => array( + 'date_on_sale_to' => array( 'description' => __( 'End data of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -1792,7 +1792,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'purchaseable' => array( + 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), @@ -1881,7 +1881,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'managing_stock' => array( + 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, @@ -2217,12 +2217,12 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'sale_price_dates_from' => array( + 'date_on_sale_from' => array( 'description' => __( 'Start date of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'sale_price_dates_to' => array( + 'date_on_sale_to' => array( 'description' => __( 'End data of sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -2233,7 +2233,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'purchaseable' => array( + 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), @@ -2298,7 +2298,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'managing_stock' => array( + 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, @@ -2453,10 +2453,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { ), ), ), - 'grouped_products' => array( + 'grouped_products_ids' => array( 'description' => __( 'List of grouped products ID.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), + 'readonly' => true, ), 'menu_order' => array( 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), From fb8773c5706d498839ec29a366bd56da72a8a4ad Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 1 Apr 2016 14:57:25 -0300 Subject: [PATCH 155/177] Group dimensions --- .../api/class-wc-rest-products-controller.php | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 0408792f3af..fadf179bdf4 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -411,9 +411,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'weight' => $product->get_weight(), - 'length' => $product->get_length(), - 'width' => $product->get_width(), - 'height' => $product->get_height(), + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), @@ -481,9 +483,11 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'backorders_allowed'=> $variation->backorders_allowed(), 'backordered' => $variation->is_on_backorder(), 'weight' => $variation->get_weight(), - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => $variation->get_shipping_class_id(), 'image' => $this->get_images( $variation ), @@ -739,18 +743,18 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Height. - if ( isset( $data['height'] ) ) { - update_post_meta( $product->id, '_height', '' === $data['height'] ? '' : wc_format_decimal( $data['height'] ) ); + if ( isset( $data['dimensions']['height'] ) ) { + update_post_meta( $product->id, '_height', '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width. - if ( isset( $data['width'] ) ) { - update_post_meta( $product->id, '_width', '' === $data['width'] ? '' : wc_format_decimal($data['width'] ) ); + if ( isset( $data['dimensions']['width'] ) ) { + update_post_meta( $product->id, '_width', '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); } // Length. - if ( isset( $data['length'] ) ) { - update_post_meta( $product->id, '_length', '' === $data['length'] ? '' : wc_format_decimal( $data['length'] ) ); + if ( isset( $data['dimensions']['length'] ) ) { + update_post_meta( $product->id, '_length', '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } @@ -1928,20 +1932,27 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'length' => array( - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'array', 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), ), 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), @@ -2339,20 +2350,27 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'length' => array( - 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), - 'type' => 'string', + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'array', 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), get_option( 'woocommerce_dimension_unit' ) ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), ), 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), From 7e791e6f4f3be751213bc2c486b17ed06570d2dc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 1 Apr 2016 15:00:00 -0300 Subject: [PATCH 156/177] Align variations params --- .../api/class-wc-rest-products-controller.php | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index fadf179bdf4..af93d88a473 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -457,41 +457,41 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } $variations[] = array( - 'id' => $variation->get_variation_id(), - 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price(), - 'date_on_sale_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', - 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', - 'on_sale' => $variation->is_on_sale(), - 'purchasable' => $variation->is_purchasable(), - 'virtual' => $variation->is_virtual(), - 'downloadable' => $variation->is_downloadable(), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => (int) $variation->download_limit, - 'download_expiry' => (int) $variation->download_expiry, - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'manage_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders' => $variation->backorders, - 'backorders_allowed'=> $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'weight' => $variation->get_weight(), - 'dimensions' => array( + 'id' => $variation->get_variation_id(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'date_on_sale_from' => $variation->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchasable' => $variation->is_purchasable(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $variation->download_limit, + 'download_expiry' => (int) $variation->download_expiry, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'manage_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->backorders, + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'dimensions' => array( 'length' => $variation->get_length(), 'width' => $variation->get_width(), 'height' => $variation->get_height(), ), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => $variation->get_shipping_class_id(), - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), ); } From 6da6c30c8656e402521af5a8eab1f3bc1e30cefc Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 4 Apr 2016 14:07:50 -0500 Subject: [PATCH 157/177] Fixed copy and paste --- includes/api/class-wc-rest-order-notes-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 09fea42c28a..2084f0ac1eb 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -254,7 +254,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { } /** - * Delete a single webhook. + * Delete a single order note. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error From 2710d12fc46ca428c31a0cfddc36eda0f0e68e9c Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 4 Apr 2016 14:13:29 -0500 Subject: [PATCH 158/177] Fixed reviews type --- includes/api/class-wc-rest-product-reviews-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/api/class-wc-rest-product-reviews-controller.php b/includes/api/class-wc-rest-product-reviews-controller.php index 0fe1a27e3ee..118e8b01875 100644 --- a/includes/api/class-wc-rest-product-reviews-controller.php +++ b/includes/api/class-wc-rest-product-reviews-controller.php @@ -155,7 +155,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { 'id' => (int) $review->comment_ID, 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), 'review' => $review->comment_content, - 'rating' => get_comment_meta( $review->comment_ID, 'rating', true ), + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), 'reviewer_name' => $review->comment_author, 'reviewer_email' => $review->comment_author_email, 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), @@ -171,7 +171,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { $response->add_links( $this->prepare_links( $review, $request ) ); /** - * Filter webhook delivery object returned from the REST API. + * Filter product reviews object returned from the REST API. * * @param WP_REST_Response $response The response object. * @param WP_Comment $review Product review object used to create response. @@ -230,7 +230,7 @@ class WC_REST_Product_Reviews_Controller extends WP_REST_Controller { ), 'rating' => array( 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), - 'type' => 'string', + 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, ), From 548c222560aa0d3c0c8091316394aa9c54ef7811 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 4 Apr 2016 14:13:38 -0500 Subject: [PATCH 159/177] Minor fixes and typos --- includes/api/class-wc-rest-order-notes-controller.php | 2 +- includes/api/class-wc-rest-order-refunds-controller.php | 4 ++-- .../api/class-wc-rest-product-attributes-controller.php | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/api/class-wc-rest-order-notes-controller.php b/includes/api/class-wc-rest-order-notes-controller.php index 2084f0ac1eb..fd142555ba7 100644 --- a/includes/api/class-wc-rest-order-notes-controller.php +++ b/includes/api/class-wc-rest-order-notes-controller.php @@ -161,7 +161,7 @@ class WC_REST_Order_Notes_Controller extends WP_REST_Controller { $args = array( 'post_id' => $order->ID, 'approve' => 'approve', - 'type' => 'order_note' + 'type' => 'order_note', ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index 36ce2c2dd19..a9d51d4c6c8 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -309,7 +309,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { if ( is_wp_error( $result ) ) { return $result; } elseif ( ! $result ) { - return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API', 'woocommerce' ), 500 ); + return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); } } } @@ -369,7 +369,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), ), 'line_items' => array( - 'description' => __( 'Line itens data.', 'woocommerce' ), + 'description' => __( 'Line items data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'properties' => array( diff --git a/includes/api/class-wc-rest-product-attributes-controller.php b/includes/api/class-wc-rest-product-attributes-controller.php index b357b463af5..0b5eb2beeec 100644 --- a/includes/api/class-wc-rest-product-attributes-controller.php +++ b/includes/api/class-wc-rest-product-attributes-controller.php @@ -254,7 +254,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); @@ -356,7 +356,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { */ do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); // Clear transients. @@ -388,7 +388,7 @@ class WC_REST_Product_Attributes_Controller extends WP_REST_Controller { return $attribute; } - $request->set_param( 'context', 'view' ); + $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $attribute, $request ); $deleted = $wpdb->delete( From d7e408d02789def3d17c87bcd0b6df576a26f345 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 4 Apr 2016 14:17:41 -0500 Subject: [PATCH 160/177] More minor fixes and typos --- includes/api/class-wc-rest-report-sales-controller.php | 6 +++--- .../api/class-wc-rest-report-top-sellers-controller.php | 2 +- includes/api/class-wc-rest-reports-controller.php | 2 +- includes/api/class-wc-rest-taxes-controller.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/api/class-wc-rest-report-sales-controller.php b/includes/api/class-wc-rest-report-sales-controller.php index 339eaf0818b..fa2c5e94f78 100644 --- a/includes/api/class-wc-rest-report-sales-controller.php +++ b/includes/api/class-wc-rest-report-sales-controller.php @@ -44,7 +44,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { protected $report; /** - * Register the routes for coupons. + * Register the routes for sales reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( @@ -123,7 +123,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { $period_totals = array(); // Setup period totals by ensuring each period in the interval has data. - for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) { + for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) { switch ( $this->report->chart_groupby ) { case 'day' : @@ -255,7 +255,7 @@ class WC_REST_Report_Sales_Controller extends WP_REST_Controller { if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { - // Iverwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. + // Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. $_GET['start_date'] = $filter['date_min']; $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; diff --git a/includes/api/class-wc-rest-report-top-sellers-controller.php b/includes/api/class-wc-rest-report-top-sellers-controller.php index e1ddf7c2804..5fee090d14b 100644 --- a/includes/api/class-wc-rest-report-top-sellers-controller.php +++ b/includes/api/class-wc-rest-report-top-sellers-controller.php @@ -161,7 +161,7 @@ class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Sales_Control 'readonly' => true, ), 'quantity' => array( - 'description' => __( 'Total of purchases.', 'woocommerce' ), + 'description' => __( 'Total number of purchases.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view' ), 'readonly' => true, diff --git a/includes/api/class-wc-rest-reports-controller.php b/includes/api/class-wc-rest-reports-controller.php index 24d0f8b4ff9..1a2062e08f6 100644 --- a/includes/api/class-wc-rest-reports-controller.php +++ b/includes/api/class-wc-rest-reports-controller.php @@ -37,7 +37,7 @@ class WC_REST_Reports_Controller extends WP_REST_Controller { protected $rest_base = 'reports'; /** - * Register the routes for coupons. + * Register the routes for reports. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( diff --git a/includes/api/class-wc-rest-taxes-controller.php b/includes/api/class-wc-rest-taxes-controller.php index 74629259357..530319e40e4 100644 --- a/includes/api/class-wc-rest-taxes-controller.php +++ b/includes/api/class-wc-rest-taxes-controller.php @@ -576,7 +576,7 @@ class WC_REST_Taxes_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit' ), ), 'priority' => array( - 'description' => __( 'Customer password.', 'woocommerce' ), + 'description' => __( 'Tax priority.', 'woocommerce' ), 'type' => 'integer', 'default' => 1, 'context' => array( 'view', 'edit' ), From 89068f0a4bde42cf75f5f18fdbd1e508c8f59afb Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Tue, 5 Apr 2016 14:58:18 -0500 Subject: [PATCH 161/177] Fixed coding standards --- includes/api/class-wc-rest-authentication.php | 2 +- .../api/class-wc-rest-products-controller.php | 38 +++++++++---------- includes/wc-rest-functions.php | 12 +++--- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/includes/api/class-wc-rest-authentication.php b/includes/api/class-wc-rest-authentication.php index fe72a038371..6d5049be133 100644 --- a/includes/api/class-wc-rest-authentication.php +++ b/includes/api/class-wc-rest-authentication.php @@ -327,7 +327,7 @@ class WC_REST_Authentication { $user = $wpdb->get_row( $wpdb->prepare( " SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces FROM {$wpdb->prefix}woocommerce_api_keys - WHERE consumer_key = '%s' + WHERE consumer_key = %s ", $consumer_key ) ); return $user; diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index af93d88a473..efc8ea857f2 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -652,7 +652,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $taxonomies = wc_get_attribute_taxonomies(); foreach ( $taxonomies as $key => $tax ) { - if ( $slug == $tax->attribute_name ) { + if ( $slug === $tax->attribute_name ) { $taxonomy = 'pa_' . $tax->attribute_name; break; @@ -674,7 +674,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $gallery = array(); foreach ( $images as $image ) { - if ( isset( $image['position'] ) && $image['position'] == 0 ) { + if ( isset( $image['position'] ) && 0 === $image['position'] ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { @@ -794,7 +794,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $files[ md5( $file_url ) ] = array( 'name' => $file_name, - 'file' => $file_url + 'file' => $file_url, ); } @@ -885,7 +885,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $sku = get_post_meta( $product->id, '_sku', true ); $new_sku = wc_clean( $request['sku'] ); - if ( '' == $new_sku ) { + if ( '' === $new_sku ) { update_post_meta( $product->id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { @@ -981,7 +981,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { if ( ! function_exists( 'attributes_cmp' ) ) { function attributes_cmp( $a, $b ) { - if ( $a['position'] == $b['position'] ) { + if ( $a['position'] === $b['position'] ) { return 0; } @@ -1043,7 +1043,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Update parent if grouped so price sorting works and stays in sync with the cheapest child. - if ( $parent_id > 0 || $product_type == 'grouped' ) { + if ( $parent_id > 0 || 'grouped' === $product_type ) { $clear_parent_ids = array(); @@ -1051,7 +1051,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $clear_parent_ids[] = $parent_id; } - if ( $product_type == 'grouped' ) { + if ( 'grouped' === $product_type ) { $clear_parent_ids[] = $product->id; } @@ -1095,7 +1095,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Stock data. - if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { // Manage stock. if ( isset( $request['manage_stock'] ) ) { $manage_stock = ( true === $request['manage_stock'] ) ? 'yes' : 'no'; @@ -1112,13 +1112,13 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $backorders = get_post_meta( $product->id, '_backorders', true ); } - if ( 'grouped' == $product_type ) { + if ( 'grouped' === $product_type ) { update_post_meta( $product->id, '_manage_stock', 'no' ); update_post_meta( $product->id, '_backorders', 'no' ); update_post_meta( $product->id, '_stock', '' ); wc_update_product_stock_status( $product->id, $stock_status ); - } elseif ( 'external' == $product_type ) { + } elseif ( 'external' === $product_type ) { update_post_meta( $product->id, '_manage_stock', 'no' ); update_post_meta( $product->id, '_backorders', 'no' ); update_post_meta( $product->id, '_stock', '' ); @@ -1126,7 +1126,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { wc_update_product_stock_status( $product->id, 'instock' ); } elseif ( 'variable' === $product_type ) { update_post_meta( $product->id, '_stock', '' ); - } elseif ( 'yes' == $manage_stock ) { + } elseif ( 'yes' === $manage_stock ) { update_post_meta( $product->id, '_backorders', $backorders ); wc_update_product_stock_status( $product->id, $stock_status ); @@ -1208,7 +1208,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Downloadable options. - if ( 'yes' == $is_downloadable ) { + if ( 'yes' === $is_downloadable ) { // Downloadable files. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { @@ -1232,7 +1232,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Product url and button text for external products. - if ( $product_type == 'external' ) { + if ( 'external' === $product_type ) { if ( isset( $request['external_url'] ) ) { update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); } @@ -1276,7 +1276,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'post_author' => get_current_user_id(), 'post_parent' => $product->id, 'post_type' => 'product_variation', - 'menu_order' => $menu_order + 'menu_order' => $menu_order, ); $variation_id = wp_insert_post( $new_variation ); @@ -1304,13 +1304,13 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $sku = get_post_meta( $variation_id, '_sku', true ); $new_sku = wc_clean( $variation['sku'] ); - if ( '' == $new_sku ) { + if ( '' === $new_sku ) { update_post_meta( $variation_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); if ( ! $unique_sku ) { - throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 ); + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); } else { update_post_meta( $variation_id, '_sku', $new_sku ); } @@ -1324,7 +1324,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { $image = current( $variation['image'] ); if ( $image && is_array( $image ) ) { - if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { + if ( isset( $image['position'] ) && isset( $image['src'] ) && 0 === $image['position'] ) { $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); if ( is_wp_error( $upload ) ) { @@ -1446,7 +1446,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } // Downloads. - if ( 'yes' == $is_downloadable ) { + if ( 'yes' === $is_downloadable ) { // Downloadable files. if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); @@ -1594,7 +1594,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $this->save_product_meta( $product, $request ); // Save variations. - if ( isset( $request['type'] ) && 'variable' == $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + if ( isset( $request['type'] ) && 'variable' === $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { $this->save_variations_data( $product, $request ); } diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 10ca418462b..17112578306 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -59,11 +59,11 @@ function wc_rest_upload_image_from_url( $image_url ) { // Check parsed URL. if ( ! $parsed_url || ! is_array( $parsed_url ) ) { - return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); } // Ensure url is valid. - $image_url = str_replace( ' ', '%20', $image_url ); + $image_url = esc_url_raw( $image_url ); // Get the file. $response = wp_safe_remote_get( $image_url, array( @@ -71,7 +71,7 @@ function wc_rest_upload_image_from_url( $image_url ) { ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { - return new WP_Error( 'woocommerce_rest_invalid_remote_image_url', sprintf( __( 'Error getting remote image %s', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_invalid_remote_image_url', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); } // Ensure we have a file name and type. @@ -101,7 +101,7 @@ function wc_rest_upload_image_from_url( $image_url ) { @unlink( $upload['file'] ); unset( $upload ); - return new WP_Error( 'woocommerce_rest_image_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_image_upload_file_error', __( 'Zero size file downloaded.', 'woocommerce' ), array( 'status' => 400 ) ); } do_action( 'woocommerce_rest_api_uploaded_image_from_url', $upload, $image_url ); @@ -140,7 +140,7 @@ function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { 'guid' => $upload['url'], 'post_parent' => $id, 'post_title' => $title, - 'post_content' => $content + 'post_content' => $content, ); $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); @@ -169,7 +169,7 @@ function wc_rest_validate_reports_request_arg( $value, $request, $param ) { $args = $attributes['args'][ $param ]; if ( 'string' === $args['type'] && ! is_string( $value ) ) { - return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%s is not of type %s', 'woocommerce' ), $param, 'string' ) ); + return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%s is not of type %s.', 'woocommerce' ), $param, 'string' ) ); } if ( 'data' === $args['format'] ) { From 157c50df0ad1400e7a2c9cdaef244d009b26c0d7 Mon Sep 17 00:00:00 2001 From: toddlahman Date: Tue, 12 Apr 2016 23:46:30 -0700 Subject: [PATCH 162/177] return boolean log add and clear method result --- includes/class-wc-logger.php | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index fb48c63680f..549bb17628d 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -7,11 +7,11 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Allows log files to be written to for debugging purposes * - * @class WC_Logger - * @version 1.6.4 - * @package WooCommerce/Classes - * @category Class - * @author WooThemes + * @class WC_Logger + * @version 1.6.4 + * @package WooCommerce/Classes + * @category Class + * @author WooThemes */ class WC_Logger { @@ -30,7 +30,6 @@ class WC_Logger { $this->_handles = array(); } - /** * Destructor. */ @@ -40,12 +39,13 @@ class WC_Logger { } } - /** * Open log file for writing. * * @access private + * * @param mixed $handle + * * @return bool success */ private function open( $handle ) { @@ -60,34 +60,44 @@ class WC_Logger { return false; } - /** * Add a log entry to chosen file. * * @param string $handle * @param string $message + * + * @return bool */ public function add( $handle, $message ) { + $result = false; + if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time - @fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); + $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time + $result = fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); } do_action( 'woocommerce_log_add', $handle, $message ); - } + return $result === false ? false : true; + } /** * Clear entries from chosen file. * * @param mixed $handle + * + * @return bool */ public function clear( $handle ) { + $result = false; + if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - @ftruncate( $this->_handles[ $handle ], 0 ); + $result = ftruncate( $this->_handles[ $handle ], 0 ); } do_action( 'woocommerce_log_clear', $handle ); + + return $result === false ? false : true; } -} +} \ No newline at end of file From 6003fb33039738deaa3443385249706904276366 Mon Sep 17 00:00:00 2001 From: toddlahman Date: Fri, 15 Apr 2016 21:26:19 -0700 Subject: [PATCH 163/177] ftruncate failed at times, so fopen() in w mode used instead. --- includes/class-wc-logger.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index 549bb17628d..bdbc079c421 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -44,16 +44,17 @@ class WC_Logger { * * @access private * - * @param mixed $handle + * @param string $handle + * @param string $mode * * @return bool success */ - private function open( $handle ) { + private function open( $handle, $mode = 'a' ) { if ( isset( $this->_handles[ $handle ] ) ) { return true; } - if ( $this->_handles[ $handle ] = @fopen( wc_get_log_file_path( $handle ), 'a' ) ) { + if ( $this->_handles[ $handle ] = @fopen( wc_get_log_file_path( $handle ), $mode ) ) { return true; } @@ -84,20 +85,24 @@ class WC_Logger { /** * Clear entries from chosen file. * - * @param mixed $handle + * @param string $handle * * @return bool */ public function clear( $handle ) { $result = false; - if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { - $result = ftruncate( $this->_handles[ $handle ], 0 ); + /** + * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at the beginning of the file, + * and truncate the file to zero length. + */ + if ( $this->open( $handle, 'w' ) && is_resource( $this->_handles[ $handle ] ) ) { + $result = true; } do_action( 'woocommerce_log_clear', $handle ); - return $result === false ? false : true; + return $result === true; } } \ No newline at end of file From 6cca7029fc5b1cd8221f7a6b6faf5f3684641a67 Mon Sep 17 00:00:00 2001 From: bucketpress Date: Mon, 18 Apr 2016 18:08:42 +0800 Subject: [PATCH 164/177] Make product tabs and ratings work when product page is loaded via ajax .wc-tabs-wrapper, .woocommerce-tabs, #rating set as delegated events, so that on ajax loading a product page, we can call $( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger('init'); to make them work. --- assets/js/frontend/single-product.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/assets/js/frontend/single-product.js b/assets/js/frontend/single-product.js index 698d03ea67a..5be817789f3 100644 --- a/assets/js/frontend/single-product.js +++ b/assets/js/frontend/single-product.js @@ -7,8 +7,8 @@ jQuery( function( $ ) { } // Tabs - $( '.wc-tabs-wrapper, .woocommerce-tabs' ) - .on( 'init', function() { + $( 'body' ) + .on( 'init', '.wc-tabs-wrapper, .woocommerce-tabs', function() { $( '.wc-tab, .woocommerce-tabs .panel:not(.panel .panel)' ).hide(); var hash = window.location.hash; @@ -34,16 +34,22 @@ jQuery( function( $ ) { $tab.closest( 'li' ).addClass( 'active' ); $tabs_wrapper.find( $tab.attr( 'href' ) ).show(); - }) - .trigger( 'init' ); + }); - $( 'a.woocommerce-review-link' ).click( function() { - $( '.reviews_tab a' ).click(); + $( 'body' ) + .on( 'click', 'a.woocommerce-review-link', function() { + $( '.reviews_tab a' ).click(); return true; }); // Star ratings for comments - $( '#rating' ).hide().before( '

12345

' ); + $( 'body' ) + .on( 'init', '#rating', function() { + $( '#rating' ).hide().before( '

12345

' ); + }); + + //Init Tabs and Rating + $( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger('init'); $( 'body' ) .on( 'click', '#respond p.stars a', function() { From 4b3d06821e3b63e45523e836754afbd165bcc503 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 18 Apr 2016 15:58:22 +0100 Subject: [PATCH 165/177] Update wording --- includes/admin/class-wc-admin-setup-wizard.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index 06f5c7a7431..d7911967871 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -563,13 +563,6 @@ class WC_Admin_Setup_Wizard { */ protected function get_wizard_payment_gateways() { $gateways = array( - 'stripe' => array( - 'name' => __( 'Stripe', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/stripe.png', - 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), - 'class' => 'featured featured-row-first', - 'repo-slug' => 'woocommerce-gateway-stripe', - ), 'paypal-braintree' => array( 'name' => __( 'PayPal by Braintree', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal-braintree.png', @@ -580,10 +573,17 @@ class WC_Admin_Setup_Wizard { 'paypal-ec' => array( 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal-express.png', - 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), 'class' => 'featured featured-row-last', 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', ), + 'stripe' => array( + 'name' => __( 'Stripe', 'woocommerce' ), + 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), + 'class' => 'featured featured-row-first', + 'repo-slug' => 'woocommerce-gateway-stripe', + ), 'paypal' => array( 'name' => __( 'PayPal Standard', 'woocommerce' ), 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), From 1439b6da31e9df0a590dd57527a46c81038dd0a8 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 18 Apr 2016 17:05:19 +0100 Subject: [PATCH 166/177] Do plugin install in the background --- .../admin/class-wc-admin-setup-wizard.php | 128 +-------------- includes/class-wc-install.php | 146 ++++++++++++++++++ 2 files changed, 148 insertions(+), 126 deletions(-) diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index d7911967871..acf47aa40e1 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -692,142 +692,18 @@ class WC_Admin_Setup_Wizard { get_wizard_payment_gateways(); - $installed_plugins = array_map( array( $this, 'format_plugin_slug' ), array_keys( get_plugins() ) ); + $gateways = $this->get_wizard_payment_gateways(); foreach ( $gateways as $gateway_id => $gateway ) { // If repo-slug is defined, download and install plugin from .org. if ( ! empty( $gateway['repo-slug'] ) && ! empty( $_POST[ 'wc-wizard-gateway-' . $gateway_id . '-enabled' ] ) ) { - $plugin_slug = $gateway['repo-slug']; - $plugin = $plugin_slug . '/' . $plugin_slug . '.php'; - $installed = false; - $activate = false; - - // See if the plugin is installed already - if ( in_array( $gateway['repo-slug'], $installed_plugins ) ) { - $installed = true; - $activate = ! is_plugin_active( $plugin ); - } - - // Install this thing! - if ( ! $installed ) { - // Suppress feedback - ob_start(); - - try { - $plugin = plugins_api( 'plugin_information', array( - 'slug' => $gateway['repo-slug'], - 'fields' => array( - 'short_description' => false, - 'sections' => false, - 'requires' => false, - 'rating' => false, - 'ratings' => false, - 'downloaded' => false, - 'last_updated' => false, - 'added' => false, - 'tags' => false, - 'homepage' => false, - 'donate_link' => false, - 'author_profile' => false, - 'author' => false, - ), - ) ); - - if ( is_wp_error( $plugin ) ) { - throw new Exception( $plugin->get_error_message() ); - } - - $package = $plugin->download_link; - $download = $upgrader->download_package( $package ); - - if ( is_wp_error( $download ) ) { - throw new Exception( $download->get_error_message() ); - } - - $working_dir = $upgrader->unpack_package( $download, true ); - - if ( is_wp_error( $working_dir ) ) { - throw new Exception( $working_dir->get_error_message() ); - } - - $result = $upgrader->install_package( array( - 'source' => $working_dir, - 'destination' => WP_PLUGIN_DIR, - 'clear_destination' => false, - 'abort_if_destination_exists' => false, - 'clear_working' => true, - 'hook_extra' => array( - 'type' => 'plugin', - 'action' => 'install', - ), - ) ); - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } - - $activate = true; - - } catch ( Exception $e ) { - WC_Admin_Notices::add_custom_notice( - $gateway_id . '_install_error', - sprintf( - __( '%s could not be installed (%s). %sPlease install it manually by clicking here.%s', 'woocommerce' ), - $gateway['name'], - $e->getMessage(), - '', - '' - ) - ); - } - - // Discard feedback - ob_end_clean(); - } - - // Activate this thing - if ( $activate ) { - try { - $result = activate_plugin( $plugin ); - - if ( is_wp_error( $result ) ) { - throw new Exception( $result->get_error_message() ); - } - } catch ( Exception $e ) { - WC_Admin_Notices::add_custom_notice( - $gateway_id . '_install_error', - sprintf( - __( '%s could not be activated (%s). %sPlease activate it manually via the plugins screen.%s', 'woocommerce' ), - $gateway['name'], - $e->getMessage(), - '', - '' - ) - ); - } - } + wp_schedule_single_event( time() + 10, 'woocommerce_plugin_background_installer', array( $gateway_id, $gateway ) ); } $settings_key = 'woocommerce_' . $gateway_id . '_settings'; diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 3ea1676c154..4d8f01ffd2b 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -41,6 +41,7 @@ class WC_Install { add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); + add_action( 'woocommerce_plugin_background_installer', array( __CLASS__, 'background_installer' ), 10, 2 ); } /** @@ -820,6 +821,151 @@ CREATE TABLE {$wpdb->prefix}woocommerce_termmeta ( return $tables; } + + /** + * Get slug from path + * @param string $key + * @return string + */ + private static function format_plugin_slug( $key ) { + $slug = explode( '/', $key ); + $slug = explode( '.', end( $slug ) ); + return $slug[0]; + } + + /** + * Install a plugin from .org in the background via a cron job (used by + * installer - opt in). + * @param string $plugin_to_install_id + * @param array $plugin_to_install + * @since 2.6.0 + */ + public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { + if ( ! empty( $plugin_to_install['repo-slug'] ) ) { + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + require_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin; + $upgrader = new WP_Upgrader( $skin ); + $installed_plugins = array_map( array( __CLASS__, 'format_plugin_slug' ), array_keys( get_plugins() ) ); + $plugin_slug = $plugin_to_install['repo-slug']; + $plugin = $plugin_slug . '/' . $plugin_slug . '.php'; + $installed = false; + $activate = false; + + // See if the plugin is installed already + if ( in_array( $plugin_to_install['repo-slug'], $installed_plugins ) ) { + $installed = true; + $activate = ! is_plugin_active( $plugin ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback + ob_start(); + + try { + $plugin_information = plugins_api( 'plugin_information', array( + 'slug' => $plugin_to_install['repo-slug'], + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) ); + + if ( is_wp_error( $plugin_information ) ) { + throw new Exception( $plugin_information->get_error_message() ); + } + + $package = $plugin_information->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%s could not be installed (%s). %sPlease install it manually by clicking here.%s', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage(), + '', + '' + ) + ); + } + + // Discard feedback + ob_end_clean(); + } + + wp_clean_plugins_cache(); + + // Activate this thing + if ( $activate ) { + try { + $result = activate_plugin( $plugin ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + __( '%s could not be activated (%s). %sPlease activate it manually via the plugins screen.%s', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage() . '"' . $plugin . '"', + '', + '' + ) + ); + } + } + } + } } WC_Install::init(); From afc060419de6627673eba1a546b5325d8272a184 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Mon, 18 Apr 2016 13:47:07 -0300 Subject: [PATCH 167/177] Pass product object to wc_attribute_label --- includes/api/class-wc-rest-products-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index efc8ea857f2..53bee01fbc9 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -318,7 +318,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. $attributes[] = array( - 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ), 'option' => $attribute, ); @@ -333,7 +333,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { } $attributes[] = array( - 'name' => wc_attribute_label( $attribute['name'] ), + 'name' => wc_attribute_label( $attribute['name'], $product ), 'slug' => str_replace( 'pa_', '', $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], From dfccfce7051fecd325bf67cc1e25810813d5143b Mon Sep 17 00:00:00 2001 From: bucketpress Date: Tue, 19 Apr 2016 10:00:16 +0800 Subject: [PATCH 168/177] Grouping of delegated events together As suggested by @claudiosmweb --- assets/js/frontend/single-product.js | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/assets/js/frontend/single-product.js b/assets/js/frontend/single-product.js index 5be817789f3..65578194dbe 100644 --- a/assets/js/frontend/single-product.js +++ b/assets/js/frontend/single-product.js @@ -34,24 +34,16 @@ jQuery( function( $ ) { $tab.closest( 'li' ).addClass( 'active' ); $tabs_wrapper.find( $tab.attr( 'href' ) ).show(); - }); - - $( 'body' ) + }) + // Review link .on( 'click', 'a.woocommerce-review-link', function() { $( '.reviews_tab a' ).click(); - return true; - }); - - // Star ratings for comments - $( 'body' ) + return true; + }) + // Star ratings for comments .on( 'init', '#rating', function() { $( '#rating' ).hide().before( '

12345

' ); - }); - - //Init Tabs and Rating - $( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger('init'); - - $( 'body' ) + }) .on( 'click', '#respond p.stars a', function() { var $star = $( this ), $rating = $( this ).closest( '#respond' ).find( '#rating' ), @@ -74,4 +66,7 @@ jQuery( function( $ ) { return false; } }); + + //Init Tabs and Star Ratings + $( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger( 'init' ); }); From 3ff10a4ce24443d54e11ba632da917df0c10e1af Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 11:10:18 +0100 Subject: [PATCH 169/177] Only get purchase note if product exists Fixes #10726 --- templates/order/order-details.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/templates/order/order-details.php b/templates/order/order-details.php index 1f0085aa50c..d3cb56ebad9 100644 --- a/templates/order/order-details.php +++ b/templates/order/order-details.php @@ -13,7 +13,7 @@ * @see http://docs.woothemes.com/document/template-structure/ * @author WooThemes * @package WooCommerce/Templates - * @version 2.5.3 + * @version 2.6.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -37,15 +37,14 @@ $show_customer_details = is_user_logged_in() && $order->get_user_id() === get_cu get_items() as $item_id => $item ) { $product = apply_filters( 'woocommerce_order_item_product', $order->get_product_from_item( $item ), $item ); - $purchase_note = get_post_meta( $product->id, '_purchase_note', true ); wc_get_template( 'order/order-details-item.php', array( - 'order' => $order, - 'item_id' => $item_id, - 'item' => $item, - 'show_purchase_note' => $show_purchase_note, - 'purchase_note' => $purchase_note, - 'product' => $product, + 'order' => $order, + 'item_id' => $item_id, + 'item' => $item, + 'show_purchase_note' => $show_purchase_note, + 'purchase_note' => $product ? get_post_meta( $product->id, '_purchase_note', true ) : '', + 'product' => $product, ) ); } ?> From 1dccf9295331955d00861f46e536a5b36073b1e7 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 11:42:44 +0100 Subject: [PATCH 170/177] Prevent changing slug to name via pointer Fixes #10727 --- includes/class-wc-order-item-meta.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/includes/class-wc-order-item-meta.php b/includes/class-wc-order-item-meta.php index 522ec449aeb..941b728e07d 100644 --- a/includes/class-wc-order-item-meta.php +++ b/includes/class-wc-order-item-meta.php @@ -116,24 +116,26 @@ class WC_Order_Item_Meta { if ( ! empty( $this->item['item_meta_array'] ) ) { foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { - if ( "" === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) == $hideprefix ) ) { + if ( "" === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { continue; } $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); + $meta_value = $meta->value; // If this is a term slug, get the term's nice name if ( taxonomy_exists( $attribute_key ) ) { - $term = get_term_by( 'slug', $meta->value, $attribute_key ); + $term = get_term_by( 'slug', $meta_value, $attribute_key ); + if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { - $meta->value = $term->name; + $meta_value = $term->name; } } $formatted_meta[ $meta_id ] = array( 'key' => $meta->key, 'label' => wc_attribute_label( $attribute_key, $this->product ), - 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta->value ), + 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value ), ); } } From 5acb2d7661b37f9dfccdc272b18eadce879bbfd7 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 12:04:52 +0100 Subject: [PATCH 171/177] Adjusts logos --- assets/images/paypal-braintree.png | Bin 17640 -> 4417 bytes assets/images/paypal-express.png | Bin 17037 -> 0 bytes assets/images/paypal.png | Bin 0 -> 46126 bytes .../admin/class-wc-admin-setup-wizard.php | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 assets/images/paypal-express.png create mode 100644 assets/images/paypal.png diff --git a/assets/images/paypal-braintree.png b/assets/images/paypal-braintree.png index 92c3bb5f2c7a7ae1fac8208d6ac0650bf75e1296..2eb31c6136f3117c04360d49046d5adfcb76cbb7 100644 GIT binary patch literal 4417 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 diff --git a/assets/images/paypal.png b/assets/images/paypal.png new file mode 100644 index 0000000000000000000000000000000000000000..a892ac1d876cd801120d9b73deaaf228a2be96b1 GIT binary patch literal 46126 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 z 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 literal 0 HcmV?d00001 diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index acf47aa40e1..348f9961cb0 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -572,7 +572,7 @@ class WC_Admin_Setup_Wizard { ), 'paypal-ec' => array( 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/paypal-express.png', + 'image' => WC()->plugin_url() . '/assets/images/paypal.png', 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), 'class' => 'featured featured-row-last', 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', From 8e0e9a0cc293d92bda20a7e25e8ba7f290ededb0 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 13:14:13 +0100 Subject: [PATCH 172/177] Deprecate Simplify in favour of feature plugin --- includes/admin/class-wc-admin-notices.php | 21 +++ .../views/html-notice-simplify-commerce.php | 24 +++ includes/class-wc-payment-gateways.php | 17 +- ...s-gateway-simplify-commerce-deprecated.php | 177 ------------------ 4 files changed, 54 insertions(+), 185 deletions(-) create mode 100644 includes/admin/views/html-notice-simplify-commerce.php delete mode 100644 includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php index d1e64cf04a8..52f766ec3fe 100644 --- a/includes/admin/class-wc-admin-notices.php +++ b/includes/admin/class-wc-admin-notices.php @@ -28,6 +28,7 @@ class WC_Admin_Notices { 'theme_support' => 'theme_check_notice', 'legacy_shipping' => 'legacy_shipping_notice', 'no_shipping_methods' => 'no_shipping_methods_notice', + 'simplify_commerce' => 'simplify_commerce_notice', ); /** @@ -57,6 +58,13 @@ class WC_Admin_Notices { if ( ! current_theme_supports( 'woocommerce' ) && ! in_array( get_option( 'template' ), wc_get_core_supported_themes() ) ) { self::add_notice( 'theme_support' ); } + + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); + + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] ) { + WC_Admin_Notices::add_notice( 'simplify_commerce' ); + } + self::add_notice( 'template_files' ); } @@ -256,6 +264,19 @@ class WC_Admin_Notices { } } } + + /** + * Simplify Commerce is being removed from core. + */ + public function simplify_commerce_notice() { + if ( class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) ) { + self::remove_notice( 'simplify_commerce' ); + return; + } + if ( empty( $_GET['action'] ) ) { + include( 'views/html-notice-simplify-commerce.php' ); + } + } } new WC_Admin_Notices(); diff --git a/includes/admin/views/html-notice-simplify-commerce.php b/includes/admin/views/html-notice-simplify-commerce.php new file mode 100644 index 00000000000..3ffc5436edd --- /dev/null +++ b/includes/admin/views/html-notice-simplify-commerce.php @@ -0,0 +1,24 @@ + +
+ + +

The Simplify Commerce payment gateway is deprecated – Please install our new free Simplify Commerce plugin from WordPress.org. Simplify Commerce will be removed from WooCommerce core in a future update.', 'woocommerce' ); ?>

+ +

+
diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php index eb20438d925..4afccce6d91 100644 --- a/includes/class-wc-payment-gateways.php +++ b/includes/class-wc-payment-gateways.php @@ -78,17 +78,18 @@ class WC_Payment_Gateways { 'WC_Gateway_Paypal', ); - $simplify_countries = (array) apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ); + /** + * Simplify Commerce is @deprecated in 2.6.0. Only load when enabled. + */ + if ( ! class_exists( 'WC_Gateway_Simplify_Commerce_Loader' ) && in_array( WC()->countries->get_base_country(), apply_filters( 'woocommerce_gateway_simplify_commerce_supported_countries', array( 'US', 'IE' ) ) ) ) { + $simplify_options = get_option( 'woocommerce_simplify_commerce_settings', array() ); - if ( in_array( WC()->countries->get_base_country(), $simplify_countries ) ) { - if ( class_exists( 'WC_Subscriptions_Order' ) || class_exists( 'WC_Pre_Orders_Order' ) ) { - if ( ! function_exists( 'wcs_create_renewal_order' ) ) { // Subscriptions < 2.0 - $load_gateways[] = 'WC_Addons_Gateway_Simplify_Commerce_Deprecated'; - } else { + if ( ! empty( $simplify_options['enabled'] ) && 'yes' === $simplify_options['enabled'] ) { + if ( function_exists( 'wcs_create_renewal_order' ) ) { $load_gateways[] = 'WC_Addons_Gateway_Simplify_Commerce'; + } else { + $load_gateways[] = 'WC_Gateway_Simplify_Commerce'; } - } else { - $load_gateways[] = 'WC_Gateway_Simplify_Commerce'; } } diff --git a/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php b/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php deleted file mode 100644 index ef7b4e89397..00000000000 --- a/includes/gateways/simplify-commerce-deprecated/class-wc-addons-gateway-simplify-commerce-deprecated.php +++ /dev/null @@ -1,177 +0,0 @@ -id, array( $this, 'process_scheduled_subscription_payment' ), 10, 3 ); - add_filter( 'woocommerce_subscriptions_renewal_order_meta_query', array( $this, 'remove_renewal_order_meta' ), 10, 4 ); - add_action( 'woocommerce_subscriptions_changed_failing_payment_method_' . $this->id, array( $this, 'change_failing_payment_method' ), 10, 3 ); - } - } - - /** - * Store the customer and card IDs on the order and subscriptions in the order. - * - * @param int $order_id - * @param string $customer_id - * @return array - */ - protected function save_subscription_meta( $order_id, $customer_id ) { - update_post_meta( $order_id, '_simplify_customer_id', wc_clean( $customer_id ) ); - } - - /** - * process_subscription_payment function. - * - * @param WC_order $order - * @param integer $amount (default: 0) - * @uses Simplify_BadRequestException - * @return bool|WP_Error - */ - public function process_subscription_payment( $order, $amount = 0 ) { - if ( 0 == $amount ) { - // Payment complete - $order->payment_complete(); - - return true; - } - - if ( $amount * 100 < 50 ) { - return new WP_Error( 'simplify_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ) ); - } - - $order_items = $order->get_items(); - $order_item = array_shift( $order_items ); - $subscription_name = sprintf( __( '%s - Subscription for "%s"', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order_item['name'] ) . ' ' . sprintf( __( '(Order #%s)', 'woocommerce' ), $order->get_order_number() ); - - $customer_id = get_post_meta( $order->id, '_simplify_customer_id', true ); - - if ( ! $customer_id ) { - return new WP_Error( 'simplify_error', __( 'Customer not found', 'woocommerce' ) ); - } - - try { - // Charge the customer - $payment = Simplify_Payment::createPayment( array( - 'amount' => $amount * 100, // In cents. - 'customer' => $customer_id, - 'description' => trim( substr( $subscription_name, 0, 1024 ) ), - 'currency' => strtoupper( get_woocommerce_currency() ), - 'reference' => $order->id, - 'card.addressCity' => $order->billing_city, - 'card.addressCountry' => $order->billing_country, - 'card.addressLine1' => $order->billing_address_1, - 'card.addressLine2' => $order->billing_address_2, - 'card.addressState' => $order->billing_state, - 'card.addressZip' => $order->billing_postcode - ) ); - - } catch ( Exception $e ) { - - $error_message = $e->getMessage(); - - if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { - $error_message = ''; - foreach ( $e->getFieldErrors() as $error ) { - $error_message .= ' ' . $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')'; - } - } - - $order->add_order_note( sprintf( __( 'Simplify payment error: %s', 'woocommerce' ), $error_message ) ); - - return new WP_Error( 'simplify_payment_declined', $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - - if ( 'APPROVED' == $payment->paymentStatus ) { - // Payment complete - $order->payment_complete( $payment->id ); - - // Add order note - $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %s, Auth Code: %s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); - - return true; - } else { - $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); - - return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - please try another card.', 'woocommerce' ) ); - } - } - - /** - * process_scheduled_subscription_payment function. - * - * @param float $amount_to_charge The amount to charge. - * @param WC_Order $order The WC_Order object of the order which the subscription was purchased in. - * @param int $product_id The ID of the subscription product for which this payment relates. - */ - public function process_scheduled_subscription_payment( $amount_to_charge, $order, $product_id ) { - $result = $this->process_subscription_payment( $order, $amount_to_charge ); - - if ( is_wp_error( $result ) ) { - WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $order, $product_id ); - } else { - WC_Subscriptions_Manager::process_subscription_payments_on_order( $order ); - } - } - - /** - * Don't transfer customer meta when creating a parent renewal order. - * - * @param string $order_meta_query MySQL query for pulling the metadata - * @param int $original_order_id Post ID of the order being used to purchased the subscription being renewed - * @param int $renewal_order_id Post ID of the order created for renewing the subscription - * @param string $new_order_role The role the renewal order is taking, one of 'parent' or 'child' - * @return string - */ - public function remove_renewal_order_meta( $order_meta_query, $original_order_id, $renewal_order_id, $new_order_role ) { - if ( 'parent' == $new_order_role ) { - $order_meta_query .= " AND `meta_key` NOT LIKE '_simplify_customer_id' "; - } - - return $order_meta_query; - } - - /** - * Check if order contains subscriptions. - * - * @param int $order_id - * @return bool - */ - protected function order_contains_subscription( $order_id ) { - return class_exists( 'WC_Subscriptions_Order' ) && ( WC_Subscriptions_Order::order_contains_subscription( $order_id ) || WC_Subscriptions_Renewal_Order::is_renewal( $order_id ) ); - } - - /** - * Update the customer_id for a subscription after using Simplify to complete a payment to make up for. - * an automatic renewal payment which previously failed. - * - * @param WC_Order $original_order The original order in which the subscription was purchased. - * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment). - * @param string $subscription_key A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key() - */ - public function change_failing_payment_method( $original_order, $renewal_order, $subscription_key ) { - $new_customer_id = get_post_meta( $renewal_order->id, '_simplify_customer_id', true ); - - update_post_meta( $original_order->id, '_simplify_customer_id', $new_customer_id ); - } -} From 03a7892da66e017fbdc5f2afdb1bf0ccc222f267 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 13:31:41 +0100 Subject: [PATCH 173/177] Learn more links to .org --- includes/admin/class-wc-admin-setup-wizard.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index 2595d764040..8077288b257 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -566,21 +566,21 @@ class WC_Admin_Setup_Wizard { 'paypal-braintree' => array( 'name' => __( 'PayPal by Braintree', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal-braintree.png', - 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s paypal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), 'class' => 'featured featured-row-last', 'repo-slug' => 'woocommerce-gateway-paypal-powered-by-braintree', ), 'paypal-ec' => array( 'name' => __( 'PayPal Express Checkout', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal.png', - 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), + 'description' => sprintf( __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. %sLearn more about PayPal%s.', 'woocommerce' ), '', '' ), 'class' => 'featured featured-row-last', 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', ), 'stripe' => array( 'name' => __( 'Stripe', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/stripe.png', - 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), + 'description' => sprintf( __( 'A modern and robust way to accept credit card payments on your store. %sLearn more about Stripe%s.', 'woocommerce' ), '', '' ), 'class' => 'featured featured-row-first', 'repo-slug' => 'woocommerce-gateway-stripe', ), From 120a62cd9092cd0c58db7241a9fa4315cfe4b467 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 13:44:20 +0100 Subject: [PATCH 174/177] minify --- assets/js/frontend/single-product.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/frontend/single-product.min.js b/assets/js/frontend/single-product.min.js index ddce18dec6b..7f9cce13f12 100644 --- a/assets/js/frontend/single-product.min.js +++ b/assets/js/frontend/single-product.min.js @@ -1 +1 @@ -jQuery(function(a){return"undefined"==typeof wc_single_product_params?!1:(a(".wc-tabs-wrapper, .woocommerce-tabs").on("init",function(){a(".wc-tab, .woocommerce-tabs .panel:not(.panel .panel)").hide();var b=window.location.hash,c=window.location.href,d=a(this).find(".wc-tabs, ul.tabs").first();b.toLowerCase().indexOf("comment-")>=0||"#reviews"===b||"#tab-reviews"===b?d.find("li.reviews_tab a").click():c.indexOf("comment-page-")>0||c.indexOf("cpage=")>0?d.find("li.reviews_tab a").click():d.find("li:first a").click()}).on("click",".wc-tabs li a, ul.tabs li a",function(b){b.preventDefault();var c=a(this),d=c.closest(".wc-tabs-wrapper, .woocommerce-tabs"),e=d.find(".wc-tabs, ul.tabs");e.find("li").removeClass("active"),d.find(".wc-tab, .panel:not(.panel .panel)").hide(),c.closest("li").addClass("active"),d.find(c.attr("href")).show()}).trigger("init"),a("a.woocommerce-review-link").click(function(){return a(".reviews_tab a").click(),!0}),a("#rating").hide().before('

12345

'),void a("body").on("click","#respond p.stars a",function(){var b=a(this),c=a(this).closest("#respond").find("#rating"),d=a(this).closest(".stars");return c.val(b.text()),b.siblings("a").removeClass("active"),b.addClass("active"),d.addClass("selected"),!1}).on("click","#respond #submit",function(){var b=a(this).closest("#respond").find("#rating"),c=b.val();return b.length>0&&!c&&"yes"===wc_single_product_params.review_rating_required?(window.alert(wc_single_product_params.i18n_required_rating_text),!1):void 0}))}); \ No newline at end of file +jQuery(function(a){return"undefined"==typeof wc_single_product_params?!1:(a("body").on("init",".wc-tabs-wrapper, .woocommerce-tabs",function(){a(".wc-tab, .woocommerce-tabs .panel:not(.panel .panel)").hide();var b=window.location.hash,c=window.location.href,d=a(this).find(".wc-tabs, ul.tabs").first();b.toLowerCase().indexOf("comment-")>=0||"#reviews"===b||"#tab-reviews"===b?d.find("li.reviews_tab a").click():c.indexOf("comment-page-")>0||c.indexOf("cpage=")>0?d.find("li.reviews_tab a").click():d.find("li:first a").click()}).on("click",".wc-tabs li a, ul.tabs li a",function(b){b.preventDefault();var c=a(this),d=c.closest(".wc-tabs-wrapper, .woocommerce-tabs"),e=d.find(".wc-tabs, ul.tabs");e.find("li").removeClass("active"),d.find(".wc-tab, .panel:not(.panel .panel)").hide(),c.closest("li").addClass("active"),d.find(c.attr("href")).show()}).on("click","a.woocommerce-review-link",function(){return a(".reviews_tab a").click(),!0}).on("init","#rating",function(){a("#rating").hide().before('

12345

')}).on("click","#respond p.stars a",function(){var b=a(this),c=a(this).closest("#respond").find("#rating"),d=a(this).closest(".stars");return c.val(b.text()),b.siblings("a").removeClass("active"),b.addClass("active"),d.addClass("selected"),!1}).on("click","#respond #submit",function(){var b=a(this).closest("#respond").find("#rating"),c=b.val();return b.length>0&&!c&&"yes"===wc_single_product_params.review_rating_required?(window.alert(wc_single_product_params.i18n_required_rating_text),!1):void 0}),void a(".wc-tabs-wrapper, .woocommerce-tabs, #rating").trigger("init"))}); \ No newline at end of file From ab61975357630f5f2213bade1386f86133deb43b Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 14:00:40 +0100 Subject: [PATCH 175/177] Tweak returns --- includes/class-wc-logger.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index bdbc079c421..9f1b93a1efd 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -79,7 +79,7 @@ class WC_Logger { do_action( 'woocommerce_log_add', $handle, $message ); - return $result === false ? false : true; + return false !== $result; } /** @@ -102,7 +102,7 @@ class WC_Logger { do_action( 'woocommerce_log_clear', $handle ); - return $result === true; + return $result; } -} \ No newline at end of file +} From 9f5967cb878818c08c482cb87fd3196c1884814d Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 16:12:22 +0100 Subject: [PATCH 176/177] Support callback and return URLs which do not define scheme @claudiosmweb Closes #10678 --- includes/class-wc-auth.php | 43 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/includes/class-wc-auth.php b/includes/class-wc-auth.php index b64274bf26d..2276771995f 100644 --- a/includes/class-wc-auth.php +++ b/includes/class-wc-auth.php @@ -134,14 +134,29 @@ class WC_Auth { $url = wc_get_endpoint_url( 'wc-auth/v' . self::VERSION, $endpoint, home_url( '/' ) ); return add_query_arg( array( - 'app_name' => wc_clean( $data['app_name'] ), - 'user_id' => wc_clean( $data['user_id'] ), - 'return_url' => urlencode( $data['return_url'] ), - 'callback_url' => urlencode( $data['callback_url'] ), - 'scope' => wc_clean( $data['scope'] ), + 'app_name' => wc_clean( $data['app_name'] ), + 'user_id' => wc_clean( $data['user_id'] ), + 'return_url' => urlencode( $this->get_formatted_url( $data['return_url'] ) ), + 'callback_url' => urlencode( $this->get_formatted_url( $data['callback_url'] ) ), + 'scope' => wc_clean( $data['scope'] ), ), $url ); } + /** + * Decode and format a URL. + * @param string $url + * @return array + */ + protected function get_formatted_url( $url ) { + $url = urldecode( $url ); + + if ( ! strstr( $url, '://' ) ) { + $url = 'https://' . $url; + } + + return $url; + } + /** * Make validation. * @@ -167,12 +182,16 @@ class WC_Auth { } foreach ( array( 'return_url', 'callback_url' ) as $param ) { - if ( false === filter_var( urldecode( $_REQUEST[ $param ] ), FILTER_VALIDATE_URL ) ) { + $param = $this->get_formatted_url( $_REQUEST[ $param ] ); + + if ( false === filter_var( $param, FILTER_VALIDATE_URL ) ) { throw new Exception( sprintf( __( 'The %s is not a valid URL', 'woocommerce' ), $param ) ); } } - if ( 0 !== stripos( urldecode( $_REQUEST['callback_url'] ), 'https://' ) ) { + $callback_url = $this->get_formatted_url( $_REQUEST['callback_url'] ); + + if ( 0 !== stripos( $callback_url, 'https://' ) ) { throw new Exception( __( 'The callback_url need to be over SSL', 'woocommerce' ) ); } } @@ -248,7 +267,7 @@ class WC_Auth { ) ); - $response = wp_safe_remote_post( esc_url_raw( urldecode( $url ) ), $params ); + $response = wp_safe_remote_post( esc_url_raw( $url ), $params ); if ( is_wp_error( $response ) ) { throw new Exception( $response->get_error_message() ); @@ -305,7 +324,7 @@ class WC_Auth { if ( 'login' == $route && ! is_user_logged_in() ) { wc_get_template( 'auth/form-login.php', array( 'app_name' => $_REQUEST['app_name'], - 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ), + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), 'redirect_url' => $this->build_url( $_REQUEST, 'authorize' ), ) ); @@ -325,7 +344,7 @@ class WC_Auth { } else if ( 'authorize' == $route && current_user_can( 'manage_woocommerce' ) ) { wc_get_template( 'auth/form-grant-access.php', array( 'app_name' => $_REQUEST['app_name'], - 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ), + 'return_url' => add_query_arg( array( 'success' => 0, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ), 'scope' => $this->get_i18n_scope( wc_clean( $_REQUEST['scope'] ) ), 'permissions' => $this->get_permissions_in_scope( wc_clean( $_REQUEST['scope'] ) ), 'granted_url' => wp_nonce_url( $this->build_url( $_REQUEST, 'access_granted' ), 'wc_auth_grant_access', 'wc_auth_nonce' ), @@ -341,10 +360,10 @@ class WC_Auth { } $consumer_data = $this->create_keys( $_REQUEST['app_name'], $_REQUEST['user_id'], $_REQUEST['scope'] ); - $response = $this->post_consumer_data( $consumer_data, $_REQUEST['callback_url'] ); + $response = $this->post_consumer_data( $consumer_data, $this->get_formatted_url( $_REQUEST['callback_url'] ) ); if ( $response ) { - wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), urldecode( $_REQUEST['return_url'] ) ) ) ); + wp_redirect( esc_url_raw( add_query_arg( array( 'success' => 1, 'user_id' => wc_clean( $_REQUEST['user_id'] ) ), $this->get_formatted_url( $_REQUEST['return_url'] ) ) ) ); exit; } } else { From f47a1de10828d3424b28e8ddd7b6356f8f2405fa Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 19 Apr 2016 16:31:36 +0100 Subject: [PATCH 177/177] Close file if it's already open before clear --- includes/class-wc-logger.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index 9f1b93a1efd..d9a8eb8f831 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -74,7 +74,7 @@ class WC_Logger { if ( $this->open( $handle ) && is_resource( $this->_handles[ $handle ] ) ) { $time = date_i18n( 'm-d-Y @ H:i:s -' ); // Grab Time - $result = fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); + $result = @fwrite( $this->_handles[ $handle ], $time . " " . $message . "\n" ); } do_action( 'woocommerce_log_add', $handle, $message ); @@ -92,6 +92,11 @@ class WC_Logger { public function clear( $handle ) { $result = false; + // Close the file if it's already open. + if ( is_resource( $this->_handles[ $handle ] ) ) { + @fclose( $handle ); + } + /** * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at the beginning of the file, * and truncate the file to zero length.