Merge pull request #4138 from woothemes/api-implementation

API implementation merge to master
This commit is contained in:
Coen Jacobs 2013-11-19 00:53:11 -08:00
commit e41687ba4f
19 changed files with 3948 additions and 24 deletions

View File

@ -26,6 +26,12 @@ class WC_Admin_Profile {
add_action( 'personal_options_update', array( $this, 'save_customer_meta_fields' ) ); add_action( 'personal_options_update', array( $this, 'save_customer_meta_fields' ) );
add_action( 'edit_user_profile_update', array( $this, 'save_customer_meta_fields' ) ); add_action( 'edit_user_profile_update', array( $this, 'save_customer_meta_fields' ) );
add_action( 'show_user_profile', array( $this, 'add_api_key_field' ) );
add_action( 'edit_user_profile', array( $this, 'add_api_key_field' ) );
add_action( 'personal_options_update', array( $this, 'generate_api_key' ) );
add_action( 'edit_user_profile_update', array( $this, 'generate_api_key' ) );
} }
/** /**
@ -176,8 +182,116 @@ class WC_Admin_Profile {
update_user_meta( $user_id, $key, woocommerce_clean( $_POST[ $key ] ) ); update_user_meta( $user_id, $key, woocommerce_clean( $_POST[ $key ] ) );
} }
/**
* Display the API key info for a user
*
* @since 2.1
* @param WP_User $user
*/
public function add_api_key_field( $user ) {
if ( ! current_user_can( 'manage_woocommerce' ) )
return;
$permissions = array(
'read' => __( 'Read', 'woocommerce' ),
'write' => __( 'Write', 'woocommerce' ),
'read_write' => __( 'Read/Write', 'woocommerce' ),
);
if ( current_user_can( 'edit_user', $user->ID ) ) {
?>
<table class="form-table">
<tbody>
<tr>
<th><label for="woocommerce_api_keys"><?php _e( 'WooCommerce API Keys', 'woocommerce' ); ?></label></th>
<td>
<?php if ( empty( $user->woocommerce_api_consumer_key ) ) : ?>
<input name="woocommerce_generate_api_key" type="checkbox" id="woocommerce_generate_api_key" value="0" />
<span class="description"><?php _e( 'Generate API Key', 'woocommerce' ); ?></span>
<?php else : ?>
<strong><?php _e( 'Consumer Key:', 'woocommerce' ); ?>&nbsp;</strong><code id="woocommerce_api_consumer_key"><?php echo $user->woocommerce_api_consumer_key ?></code><br/>
<strong><?php _e( 'Consumer Secret:', 'woocommerce' ); ?>&nbsp;</strong><code id="woocommerce_api_consumer_secret"><?php echo $user->woocommerce_api_consumer_secret; ?></code><br/>
<strong><?php _e( 'Permissions:', 'woocommerce' ); ?>&nbsp;</strong><span id="woocommerce_api_key_permissions"><select name="woocommerce_api_key_permissions" id="woocommerce_api_key_permissions"><?php
foreach ( $permissions as $permission_key => $permission_name ) { echo '<option value="'.$permission_key.'" '.selected($permission_key, $user->woocommerce_api_key_permissions, false).'>'.esc_html( $permission_name ) . '</option>';} ?>
</select></span><br/>
<input name="woocommerce_generate_api_key" type="checkbox" id="woocommerce_generate_api_key" value="0" />
<span class="description"><?php _e( 'Revoke API Key', 'woocommerce' ); ?></span>
<?php endif; ?>
</td>
</tr>
</tbody>
</table>
<?php
}
}
/**
* Generate and save (or delete) the API keys for a user
*
* @since 2.1
* @param int $user_id
*/
public function generate_api_key( $user_id ) {
if ( current_user_can( 'edit_user', $user_id ) ) {
$user = wp_get_current_user();
// creating/deleting key
if ( isset( $_POST['woocommerce_generate_api_key'] ) ) {
// consumer key
if ( empty( $user->woocommerce_api_consumer_key ) ) {
$consumer_key = 'ck_' . hash( 'md5', $user->user_login . date( 'U' ) . mt_rand() );
update_user_meta( $user_id, 'woocommerce_api_consumer_key', $consumer_key );
} else {
delete_user_meta( $user_id, 'woocommerce_api_consumer_key' );
}
// consumer secret
if ( empty( $user->woocommerce_api_consumer_secret ) ) {
$consumer_secret = 'cs_' . hash( 'md5', $user->ID . date( 'U' ) . mt_rand() );
update_user_meta( $user_id, 'woocommerce_api_consumer_secret', $consumer_secret );
} else {
delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' );
}
// permissions
if ( empty( $user->woocommerce_api_key_permissions ) ) {
$permissions = ( ! in_array( $_POST['woocommerce_api_key_permissions'], array( 'read', 'write', 'read_write' ) ) ) ? 'read' : $_POST['woocommerce_api_key_permissions'];
update_user_meta( $user_id, 'woocommerce_api_key_permissions', $permissions );
} else {
delete_user_meta( $user_id, 'woocommerce_api_key_permissions' );
}
} else {
// updating permissions for key
if ( ! empty( $_POST['woocommerce_api_key_permissions'] ) && $user->woocommerce_api_key_permissions !== $_POST['woocommerce_api_key_permissions'] ) {
$permissions = ( ! in_array( $_POST['woocommerce_api_key_permissions'], array( 'read', 'write', 'read_write' ) ) ) ? 'read' : $_POST['woocommerce_api_key_permissions'];
update_user_meta( $user_id, 'woocommerce_api_key_permissions', $permissions );
}
}
}
}
} }
endif; endif;
return new WC_Admin_Profile(); return new WC_Admin_Profile();

View File

@ -770,4 +770,4 @@ class WC_Admin_Settings {
} }
} }
endif; endif;

View File

@ -99,6 +99,14 @@ class WC_Settings_General extends WC_Settings_Page {
'autoload' => false 'autoload' => false
), ),
array(
'title' => __( 'API', 'woocommerce' ),
'desc' => __( 'Enable the REST API', 'woocommerce' ),
'id' => 'woocommerce_api_enabled',
'type' => 'checkbox',
'default' => 'yes',
),
array( 'type' => 'sectionend', 'id' => 'general_options'), array( 'type' => 'sectionend', 'id' => 'general_options'),
array( 'title' => __( 'Currency Options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ), 'id' => 'pricing_options' ), array( 'title' => __( 'Currency Options', 'woocommerce' ), 'type' => 'title', 'desc' => __( 'The following options affect how prices are displayed on the frontend.', 'woocommerce' ), 'id' => 'pricing_options' ),
@ -293,4 +301,4 @@ class WC_Settings_General extends WC_Settings_Page {
endif; endif;
return new WC_Settings_General(); return new WC_Settings_General();

View File

@ -0,0 +1,298 @@
<?php
/**
* WooCommerce API Authentication Class
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Authentication {
/**
* Setup class
*
* @since 2.1
* @return WC_API_Authentication
*/
public function __construct() {
// this filter can be removed in order to provide unauthenticated access to the API for testing, etc
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ) );
}
/**
* Authenticate the request. The authentication method varies based on whether the request was made over SSL or not.
*
* @since 2.1
* @param WP_User $user
* @return null|WP_Error|WP_User
*/
public function authenticate( $user ) {
// allow access to the index by default
if ( '/' === WC()->api->server->path )
return new WP_User(0);
try {
if ( is_ssl() )
$user = $this->perform_ssl_authentication();
else
$user = $this->perform_oauth_authentication();
// check API key-specific permission
$this->check_api_key_permissions( $user );
} catch ( Exception $e ) {
$user = new WP_Error( 'woocommerce_api_authentication_error', $e->getMessage(), array( 'status' => $e->getCode() ) );
}
return $user;
}
/**
* 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
*
* @since 2.1
* @return WP_User
* @throws Exception
*/
private function perform_ssl_authentication() {
if ( empty( $_SERVER['PHP_AUTH_USER'] ) )
throw new Exception( __( 'Consumer Key is missing', 'woocommerce' ), 404 );
if ( empty( $_SERVER['PHP_AUTH_PW'] ) )
throw new Exception( __( 'Consumer Secret is missing', 'woocommerce' ), 404 );
$consumer_key = $_SERVER['PHP_AUTH_USER'];
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
$user = $this->get_user_by_consumer_key( $consumer_key );
if ( ! $this->is_consumer_secret_valid( $user, $consumer_secret ) )
throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce'), 401 );
return $user;
}
/**
* 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 WP_User
* @throws Exception
*/
private function perform_oauth_authentication() {
$params = WC()->api->server->params['GET'];
$param_names = array( 'oauth_consumer_key', 'oauth_timestamp', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method' );
// check for required OAuth parameters
foreach ( $param_names as $param_name ) {
if ( empty( $params ) )
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ) );
}
// fetch WP user by consumer key
$user = $this->get_user_by_consumer_key( $params['oauth_consumer_key'] );
// perform OAuth validation
$this->check_oauth_signature( $user, $params );
$this->check_oauth_timestamp_and_nonce( $user, $params['oauth_timestamp'], $params['oauth_nonce'] );
// remove oauth params before further parsing
foreach( $param_names as $param_name ) {
unset( WC()->api->server->params[ $param_name ] );
}
// authentication successful, return user
return $user;
}
/**
* Return the user for the given consumer key
*
* @since 2.1
* @param string $consumer_key
* @return WP_User
* @throws Exception
*/
private function get_user_by_consumer_key( $consumer_key ) {
$user_query = new WP_User_Query(
array(
'meta_key' => 'woocommerce_api_consumer_key',
'meta_value' => $consumer_key,
)
);
$users = $user_query->get_results();
if ( empty( $users[0] ) )
throw new Exception( __( 'Consumer Key is invalid', 'woocommerce' ), 401 );
return $users[0];
}
/**
* Check if the consumer secret provided for the given user is valid
*
* @since 2.1
* @param WP_User $user
* @param string $consumer_secret
* @return bool
*/
private function is_consumer_secret_valid( WP_User $user, $consumer_secret ) {
return $user->woocommerce_api_consumer_secret === $consumer_secret;
}
/**
* Verify that the consumer-provided request signature matches our generated signature, this ensures the consumer
* has a valid key/secret
*
* @param WP_User $user
* @param array $params the request parameters
* @throws Exception
*/
private function check_oauth_signature( $user, $params ) {
$http_method = strtoupper( WC()->api->server->method );
$base_request_uri = rawurlencode( get_home_url( null, parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), 'http' ) );
// 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'] );
// normalize parameter key/values
array_walk( $params, array( $this, 'normalize_parameters' ) );
// sort parameters
if ( ! uksort( $params, 'strcmp' ) )
throw new Exception( __( 'Invalid Signature - failed to sort parameters', 'woocommerce' ), 401 );
// form query string
$query_params = array();
foreach ( $params as $param_key => $param_value ) {
$query_params[] = $param_key . '%3D' . $param_value; // join with equals sign
}
$query_string = implode( '%26', $query_params ); // 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' )
throw new Exception( __( 'Invalid Signature - signature method is invalid', 'woocommerce' ), 401 );
$hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
$signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $user->woocommerce_api_consumer_secret, true ) );
if ( $signature !== $consumer_signature )
throw new Exception( __( 'Invalid Signature - provided signature does not match', 'woocommerce' ), 401 );
}
/**
* Normalize each parameter by assuming each parameter may have already been encoded, so attempt to decode, and then
* re-encode according to RFC 3986
*
* @since 2.1
* @see rawurlencode()
* @param string $key
* @param string $value
*/
private function normalize_parameters( &$key, &$value ) {
$key = rawurlencode( rawurldecode( $key ) );
$value = rawurlencode( rawurldecode( $value ) );
}
/**
* 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 WP_User $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
* @throws Exception
*/
private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) {
$valid_window = 15 * 60; // 15 minute window
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) )
throw new Exception( __( 'Invalid timestamp', 'woocommerce' ) );
$used_nonces = $user->woocommerce_api_nonces;
if ( empty( $used_nonces ) )
$used_nonces = array();
if ( in_array( $nonce, $used_nonces ) )
throw new Exception( __( 'Invalid nonce - nonce has already been used', 'woocommerce' ), 401 );
$used_nonces[ $timestamp ] = $nonce;
// remove expired nonces
foreach( $used_nonces as $nonce_timestamp => $nonce ) {
if ( $nonce_timestamp < $valid_window )
unset( $used_nonces[ $nonce_timestamp ] );
}
update_user_meta( $user->ID, 'woocommerce_api_nonces', $used_nonces );
}
/**
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
*
* @param WP_User $user
* @throws Exception if the permission check fails
*/
public function check_api_key_permissions( $user ) {
$key_permissions = $user->woocommerce_api_key_permissions;
switch ( WC()->api->server->method ) {
case 'HEAD':
case 'GET':
if ( 'read' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have read permissions', 'woocommerce' ), 401 );
}
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
if ( 'write' !== $key_permissions && 'read_write' !== $key_permissions ) {
throw new Exception( __( 'The API key provided does not have write permissions', 'woocommerce' ), 401 );
}
break;
}
}
}

View File

@ -0,0 +1,253 @@
<?php
/**
* WooCommerce API Coupons Class
*
* Handles requests to the /coupons endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Coupons extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/coupons';
/**
* Register the routes for this class
*
* GET|POST /coupons
* GET /coupons/count
* GET|PUT|DELETE /coupons/<id>
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET|POST /coupons
$routes[ $this->base ] = array(
array( array( $this, 'get_coupons' ), WC_API_Server::READABLE ),
array( array( $this, 'create_coupon' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
);
# GET /coupons/count
$routes[ $this->base . '/count'] = array(
array( array( $this, 'get_coupons_count' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /coupons/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_coupon' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_coupon' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_coupon' ), WC_API_Server::DELETABLE ),
);
# GET /coupons/code/<code>, note that coupon codes can contain spaces, dashes and underscores
$routes[ $this->base . '/code/(?P<code>\w[\w\s\-]*)' ] = array(
array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all coupons
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_coupons( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_coupons( $filter );
$coupons = array();
foreach( $query->posts as $coupon_id ) {
if ( ! $this->is_readable( $coupon_id ) )
continue;
$coupons[] = $this->get_coupon( $coupon_id, $fields );
}
$this->server->add_pagination_headers( $query );
return array( 'coupons' => $coupons );
}
/**
* Get the coupon for the given ID
*
* @since 2.1
* @param int $id the coupon ID
* @param string $fields fields to include in response
* @return array|WP_Error
*/
public function get_coupon( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'shop_coupon', 'read' );
if ( is_wp_error( $id ) )
return $id;
// get the coupon code
$code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $id ) );
if ( is_null( $code ) )
return new WP_Error( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), array( 'status' => 404 ) );
$coupon = new WC_Coupon( $code );
$coupon_post = get_post( $coupon->id );
$coupon_data = array(
'id' => $coupon->id,
'code' => $coupon->code,
'type' => $coupon->type,
'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ),
'amount' => woocommerce_format_decimal( $coupon->amount ),
'individual_use' => $coupon->individual_use,
'product_ids' => $coupon->product_ids,
'exclude_product_ids' => $coupon->exclude_product_ids,
'usage_limit' => $coupon->usage_limit,
'usage_limit_per_user' => $coupon->usage_limit_per_user,
'limit_usage_to_x_items' => $coupon->limit_usage_to_x_items,
'usage_count' => $coupon->usage_count,
'expiry_date' => $this->server->format_datetime( $coupon->expiry_date ),
'apply_before_tax' => $coupon->apply_before_tax(),
'enable_free_shipping' => $coupon->enable_free_shipping(),
'product_categories' => $coupon->product_categories,
'exclude_product_categories' => $coupon->exclude_product_categories,
'exclude_sale_items' => $coupon->exclude_sale_items(),
'minimum_amount' => $coupon->minimum_amount,
'customer_email' => $coupon->customer_email,
);
return apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server );
}
/**
* Get the total number of coupons
*
* @since 2.1
* @param array $filter
* @return array
*/
public function get_coupons_count( $filter = array() ) {
$query = $this->query_coupons( $filter );
// TODO: permissions?
return array( 'count' => $query->found_posts );
}
/**
* Get the coupon for the given code
*
* @since 2.1
* @param string $code the coupon code
* @param string $fields fields to include in response
* @return int|WP_Error
*/
public function get_coupon_by_code( $code, $fields = null ) {
global $wpdb;
$id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $code ) );
if ( is_null( $id ) )
return new WP_Error( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), array( 'status' => 404 ) );
return $this->get_coupon( $id, $fields );
}
/**
* Create a coupon
*
* @since 2.1
* @param array $data
* @return array
*/
public function create_coupon( $data ) {
// TODO: permissions check
// TODO: implement - what's the minimum set of data required?
return array();
}
/**
* Edit a coupon
*
* @since 2.1
* @param int $id the coupon ID
* @param array $data
* @return array
*/
public function edit_coupon( $id, $data ) {
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
if ( is_wp_error( $id ) )
return $id;
// TODO: implement
return $this->get_coupon( $id );
}
/**
* Delete a coupon
*
* @since 2.1
* @param int $id the coupon ID
* @param bool $force true to permanently delete coupon, false to move to trash
* @return array
*/
public function delete_coupon( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_coupon', 'delete' );
if ( is_wp_error( $id ) )
return $id;
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
}
/**
* Helper method to get coupon post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_coupons( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
);
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
}

View File

@ -0,0 +1,475 @@
<?php
/**
* WooCommerce API Customers Class
*
* Handles requests to the /customers endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Customers extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/customers';
/** @var string $created_at_min for date filtering */
private $created_at_min = null;
/** @var string $created_at_max for date filtering */
private $created_at_max = null;
/**
* Setup class, overridden to provide customer data to order response
*
* @since 2.1
* @param WC_API_Server $server
* @return WC_API_Customers
*/
public function __construct( WC_API_Server $server ) {
parent::__construct( $server );
// add customer data to order responses
add_filter( 'woocommerce_api_order_response', array( $this, 'add_customer_data' ), 10, 2 );
// modify WP_User_Query to support created_at date filtering
add_action( 'pre_user_query', array( $this, 'modify_user_query' ) );
}
/**
* Register the routes for this class
*
* GET|POST /customers
* GET /customers/count
* GET|PUT|DELETE /customers/<id>
* GET /customers/<id>/orders
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET|POST /customers
$routes[ $this->base ] = array(
array( array( $this, 'get_customers' ), WC_API_SERVER::READABLE ),
array( array( $this, 'create_customer' ), WC_API_SERVER::CREATABLE | WC_API_SERVER::ACCEPT_DATA ),
);
# GET /customers/count
$routes[ $this->base . '/count'] = array(
array( array( $this, 'get_customers_count' ), WC_API_SERVER::READABLE ),
);
# GET|PUT|DELETE /customers/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_customer' ), WC_API_SERVER::READABLE ),
array( array( $this, 'edit_customer' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ),
array( array( $this, 'delete_customer' ), WC_API_SERVER::DELETABLE ),
);
# GET /customers/<id>/orders
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
);
return $routes;
}
/**
* Get all customers
*
* @since 2.1
* @param array $fields
* @param array $filter
* @param int $page
* @return array
*/
public function get_customers( $fields = null, $filter = array(), $page = 1 ) {
$filter['page'] = $page;
$query = $this->query_customers( $filter );
$customers = array();
foreach( $query->get_results() as $user_id ) {
if ( ! $this->is_readable( $user_id ) )
continue;
$customers[] = $this->get_customer( $user_id, $fields );
}
$this->server->add_pagination_headers( $query );
return array( 'customers' => $customers );
}
/**
* Get the customer for the given ID
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields
* @return array
*/
public function get_customer( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) )
return $id;
$customer = new WP_User( $id );
// get info about user's last order
$last_order = $wpdb->get_row( "SELECT id, post_date_gmt
FROM $wpdb->posts AS posts
LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
WHERE meta.meta_key = '_customer_user'
AND meta.meta_value = {$customer->ID}
AND posts.post_type = 'shop_order'
AND posts.post_status = 'publish'
" );
$customer_data = array(
'id' => $customer->ID,
'created_at' => $this->server->format_datetime( $customer->user_registered ),
'email' => $customer->user_email,
'first_name' => $customer->first_name,
'last_name' => $customer->last_name,
'username' => $customer->user_login,
'last_order_id' => is_object( $last_order ) ? $last_order->id : null,
'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null,
'orders_count' => (int) $customer->_order_count,
'total_spent' => woocommerce_format_decimal( $customer->_money_spent ),
'avatar_url' => $this->get_avatar_url( $customer->customer_email ),
'billing_address' => array(
'first_name' => $customer->billing_first_name,
'last_name' => $customer->billing_last_name,
'company' => $customer->billing_company,
'address_1' => $customer->billing_address_1,
'address_2' => $customer->billing_address_2,
'city' => $customer->billing_city,
'state' => $customer->billing_state,
'postcode' => $customer->billing_postcode,
'country' => $customer->billing_country,
'email' => $customer->billing_email,
'phone' => $customer->billing_phone,
),
'shipping_address' => array(
'first_name' => $customer->shipping_first_name,
'last_name' => $customer->shipping_last_name,
'company' => $customer->shipping_company,
'address_1' => $customer->shipping_address_1,
'address_2' => $customer->shipping_address_2,
'city' => $customer->shipping_city,
'state' => $customer->shipping_state,
'postcode' => $customer->shipping_postcode,
'country' => $customer->shipping_country,
),
);
return apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server );
}
/**
* Get the total number of customers
*
* @since 2.1
* @param array $filter
* @return array
*/
public function get_customers_count( $filter = array() ) {
$query = $this->query_customers( $filter );
if ( ! current_user_can( 'list_users' ) )
return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read customers', 'woocommerce' ), array( 'status' => 401 ) );
return array( 'count' => count( $query->get_results() ) );
}
/**
* Create a customer
*
* @since 2.1
* @param array $data
* @return array
*/
public function create_customer( $data ) {
if ( ! current_user_can( 'create_users' ) )
return new WP_Error( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), array( 'status' => 401 ) );
// TODO: implement - woocommerce_create_new_customer()
return array();
}
/**
* Edit a customer
*
* @since 2.1
* @param int $id the customer ID
* @param array $data
* @return array
*/
public function edit_customer( $id, $data ) {
$id = $this->validate_request( $id, 'customer', 'edit' );
if ( ! is_wp_error( $id ) )
return $id;
// TODO: implement
return $this->get_customer( $id );
}
/**
* Delete a customer
*
* @since 2.1
* @param int $id the customer ID
* @return array
*/
public function delete_customer( $id ) {
$id = $this->validate_request( $id, 'customer', 'delete' );
if ( ! is_wp_error( $id ) )
return $id;
return $this->delete( $id, 'customer' );
}
/**
* Get the orders for a customer
*
* @since 2.1
* @param int $id the customer ID
* @param string $fields fields to include in response
* @return array
*/
public function get_customer_orders( $id, $fields = null ) {
global $wpdb;
$id = $this->validate_request( $id, 'customer', 'read' );
if ( is_wp_error( $id ) )
return $id;
$order_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id
FROM $wpdb->posts AS posts
LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id
WHERE meta.meta_key = '_customer_user'
AND meta.meta_value = '%s'
AND posts.post_type = 'shop_order'
AND posts.post_status = 'publish'
", $id ) );
if ( empty( $order_ids ) )
return array( 'orders' => array() );
$orders = array();
foreach ( $order_ids as $order_id ) {
$orders[] = WC()->api->WC_API_Orders->get_order( $order_id, $fields );
}
return array( 'orders' => apply_filters( 'woocommerce_api_customer_orders_response', $orders, $id, $fields, $order_ids, $this->server ) );
}
/**
* Helper method to get customer user objects
*
* Note that WP_User_Query does not have built-in pagination so limit & offset are used to provide limited
* pagination support
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return array
*/
private function query_customers( $args = array() ) {
// default users per page
$users_per_page = get_option( 'posts_per_page' );
// set base query arguments
$query_args = array(
'fields' => 'ID',
'role' => 'customer',
'orderby' => 'registered',
'number' => $users_per_page,
);
// search
if ( ! empty( $args['q'] ) ) {
$query_args['search'] = $args['q'];
}
// limit number of users returned
if ( ! empty( $args['limit'] ) ) {
$query_args['number'] = absint( $args['limit'] );
$users_per_page = absint( $args['limit'] );
}
// page
$page = absint( $args['page'] );
// offset
if ( ! empty( $args['offset'] ) ) {
$query_args['offset'] = absint( $args['offset'] );
} else {
$query_args['offset'] = $users_per_page * ( $page - 1 );
}
// created date
if ( ! empty( $args['created_at_min'] ) ) {
$this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] );
}
if ( ! empty( $args['created_at_max'] ) ) {
$this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] );
}
$query = new WP_User_Query( $query_args );
// helper members for pagination headers
$query->total_pages = ceil( $query->get_total() / $users_per_page );
$query->page = $page;
return $query;
}
/**
* Add customer data to orders
*
* @since 2.1
* @param $order_data
* @param $order
* @return array
*/
public function add_customer_data( $order_data, $order ) {
if ( 0 == $order->customer_user ) {
$order_data['customer'] = 'guest';
} else {
$order_data['customer'] = $this->get_customer( $order->customer_user );
}
return $order_data;
}
/**
* Modify the WP_User_Query to support filtering on the date the customer was created
*
* @since 2.1
* @param WP_User_Query $query
*/
public function modify_user_query( $query ) {
if ( $this->created_at_min )
$query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) );
if ( $this->created_at_max )
$query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) );
}
/**
* Wrapper for @see get_avatar() which doesn't simply return
* the URL so we need to pluck it from the HTML img tag
*
* @since 2.1
* @param string $email the customer's email
* @return string the URL to the customer's avatar
*/
private function get_avatar_url( $email ) {
$dom = new DOMDocument();
$dom->loadHTML( get_avatar( $email ) );
$url = $dom->getElementsByTagName( 'img' )->item( 0 )->getAttribute( 'src' );
return ( ! empty( $url ) ) ? $url : null;
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid WP_User
* 3) the current user has the proper permissions
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param string|int $id the customer ID
* @param string $type the request type, unused because this method overrides the parent class
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid user ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
$id = absint( $id );
// validate ID
if ( empty( $id ) )
return new WP_Error( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), array( 'status' => 404 ) );
// non-existent IDs return a valid WP_User object with the user ID = 0
$customer = new WP_User( $id );
if ( 0 === $customer->ID )
return new WP_Error( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), array( 'status' => 404 ) );
// validate permissions
switch ( $context ) {
case 'read':
if ( ! current_user_can( 'list_users' ) )
return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), array( 'status' => 401 ) );
break;
case 'edit':
if ( ! current_user_can( 'edit_users' ) )
return new WP_Error( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), array( 'status' => 401 ) );
break;
case 'delete':
if ( ! current_user_can( 'delete_users' ) )
return new WP_Error( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), array( 'status' => 401 ) );
break;
}
return $id;
}
/**
* Check if the current user can read users
*
* @since 2.1
* @see WC_API_Resource::is_readable()
* @param int|WP_Post $post unused
* @return bool true if the current user can read users, false otherwise
*/
protected function is_readable( $post ) {
return current_user_can( 'list_users' );
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* WooCommerce API
*
* Handles parsing JSON request bodies and generating JSON responses
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_JSON_Handler implements WC_API_Handler {
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return 'application/json; charset=' . get_option( 'blog_charset' );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $body the raw request body
* @return array|mixed
*/
public function parse_body( $body ) {
return json_decode( $body, true );
}
/**
* Generate a JSON response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
if ( isset( $_GET['_jsonp'] ) ) {
// JSONP enabled by default
if ( ! apply_filters( 'woocommerce_api_jsonp_enabled', true ) ) {
WC()->api->server->send_status( 400 );
$data = array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) );
}
// Check for invalid characters (only alphanumeric allowed)
if ( preg_match( '/\W/', $_GET['_jsonp'] ) ) {
WC()->api->server->send_status( 400 );
$data = array( array( 'code' => 'woocommerce_api_json_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) );
}
return $_GET['_jsonp'] . '(' . json_encode( $data ) . ')';
}
return json_encode( $data );
}
}

View File

@ -0,0 +1,374 @@
<?php
/**
* WooCommerce API Orders Class
*
* Handles requests to the /orders endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Orders extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/orders';
/**
* Register the routes for this class
*
* GET /orders
* GET /orders/count
* GET|PUT|DELETE /orders/<id>
* GET /orders/<id>/notes
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /orders
$routes[ $this->base ] = array(
array( array( $this, 'get_orders' ), WC_API_Server::READABLE ),
);
# GET /orders/count
$routes[ $this->base . '/count'] = array(
array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /orders/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_order' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ),
);
# GET /orders/<id>/notes
$routes[ $this->base . '/(?P<id>\d+)/notes' ] = array(
array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all orders
*
* @since 2.1
* @param string $fields
* @param array $filter
* @param string $status
* @param int $page
* @return array
*/
public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) {
if ( ! empty( $status ) )
$filter['status'] = $status;
$filter['page'] = $page;
$query = $this->query_orders( $filter );
$orders = array();
foreach( $query->posts as $order_id ) {
if ( ! $this->is_readable( $order_id ) )
continue;
$orders[] = $this->get_order( $order_id, $fields );
}
$this->server->add_pagination_headers( $query );
return array( 'orders' => $orders );
}
/**
* Get the order for the given ID
*
* @since 2.1
* @param int $id the order ID
* @param array $fields
* @return array
*/
public function get_order( $id, $fields = null ) {
// ensure order ID is valid & user has permission to read
$id = $this->validate_request( $id, 'shop_order', 'read' );
if ( is_wp_error( $id ) )
return $id;
$order = new WC_Order( $id );
$order_post = get_post( $id );
$order_data = array(
'id' => $order->id,
'order_number' => $order->get_order_number(),
'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ),
'completed_at' => $this->server->format_datetime( $order->completed_date, true ),
'status' => $order->status,
'currency' => $order->order_currency,
'total' => woocommerce_format_decimal( $order->get_total() ),
'total_line_items_quantity' => $order->get_item_count(),
'total_tax' => woocommerce_format_decimal( $order->get_total_tax() ),
'total_shipping' => woocommerce_format_decimal( $order->get_total_shipping() ),
'cart_tax' => woocommerce_format_decimal( $order->get_cart_tax() ),
'shipping_tax' => woocommerce_format_decimal( $order->get_shipping_tax() ),
'total_discount' => woocommerce_format_decimal( $order->get_total_discount() ),
'cart_discount' => woocommerce_format_decimal( $order->get_cart_discount() ),
'order_discount' => woocommerce_format_decimal( $order->get_order_discount() ),
'shipping_methods' => $order->get_shipping_method(),
'payment_details' => array(
'method_id' => $order->payment_method,
'method_title' => $order->payment_method_title,
'paid' => isset( $order->paid_date ),
),
'billing_address' => array(
'first_name' => $order->billing_first_name,
'last_name' => $order->billing_last_name,
'company' => $order->billing_company,
'address_1' => $order->billing_address_1,
'address_2' => $order->billing_address_2,
'city' => $order->billing_city,
'state' => $order->billing_state,
'postcode' => $order->billing_postcode,
'country' => $order->billing_country,
'email' => $order->billing_email,
'phone' => $order->billing_phone,
),
'shipping_address' => array(
'first_name' => $order->shipping_first_name,
'last_name' => $order->shipping_last_name,
'company' => $order->shipping_company,
'address_1' => $order->shipping_address_1,
'address_2' => $order->shipping_address_2,
'city' => $order->shipping_city,
'state' => $order->shipping_state,
'postcode' => $order->shipping_postcode,
'country' => $order->shipping_country,
),
'note' => $order->customer_note,
'customer_ip' => $order->customer_ip_address,
'customer_user_agent' => $order->customer_user_agent,
'customer_id' => $order->customer_user,
'view_order_url' => $order->get_view_order_url(),
'line_items' => array(),
'shipping_lines' => array(),
'tax_lines' => array(),
'fee_lines' => array(),
'coupon_lines' => array(),
);
// add line items
foreach( $order->get_items() as $item_id => $item ) {
$product = $order->get_product_from_item( $item );
$order_data['line_items'][] = array(
'id' => $item_id,
'subtotal' => woocommerce_format_decimal( $order->get_line_subtotal( $item ) ),
'total' => woocommerce_format_decimal( $order->get_line_total( $item ) ),
'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $item ) ),
'quantity' => (int) $item['qty'],
'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null,
'name' => $item['name'],
'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id,
'sku' => $product->get_sku(),
);
}
// add shipping
foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
$order_data['shipping_lines'][] = array(
'id' => $shipping_item_id,
'method_id' => $shipping_item['method_id'],
'method_title' => $shipping_item['name'],
'total' => woocommerce_format_decimal( $shipping_item['cost'] ),
);
}
// add taxes
foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
$order_data['tax_lines'][] = array(
'code' => $tax_code,
'title' => $tax->label,
'total' => woocommerce_format_decimal( $tax->amount ),
'compound' => (bool) $tax->is_compound,
);
}
// add fees
foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
$order_data['fee_lines'] = array(
'id' => $fee_item_id,
'title' => $fee_item['name'],
'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null,
'total' => woocommerce_format_decimal( $order->get_line_total( $fee_item ) ),
'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $fee_item ) ),
);
}
// add coupons
foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
$order_data['coupon_lines'] = array(
'id' => $coupon_item_id,
'code' => $coupon_item['name'],
'amount' => woocommerce_format_decimal( $coupon_item['discount_amount'] ),
);
}
return apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server );
}
/**
* Get the total number of orders
*
* @since 2.1
* @param string $status
* @param array $filter
* @return array
*/
public function get_orders_count( $status = null, $filter = array() ) {
if ( ! empty( $status ) )
$filter['status'] = $status;
$query = $this->query_orders( $filter );
// TODO: permissions?
return array( 'count' => $query->found_posts );
}
/**
* Edit an order
*
* @since 2.1
* @param int $id the order ID
* @param array $data
* @return array
*/
public function edit_order( $id, $data ) {
$id = $this->validate_request( $id, 'shop_order', 'write' );
if ( is_wp_error( $id ) )
return $id;
// TODO: implement, especially for status change
return $this->get_order( $id );
}
/**
* Delete an order
*
* @since 2.1
* @param int $id the order ID
* @param bool $force true to permanently delete order, false to move to trash
* @return array
*/
public function delete_order( $id, $force = false ) {
$id = $this->validate_request( $id, 'shop_order', 'delete' );
return $this->delete( $id, 'order', ( 'true' === $force ) );
}
/**
* Get the admin order notes for an order
*
* @since 2.1
* @param int $id the order ID
* @param string $fields fields to include in response
* @return array
*/
public function get_order_notes( $id, $fields = null ) {
// ensure ID is valid order ID
$id = $this->validate_request( $id, 'order', 'read' );
if ( is_wp_error( $id ) )
return $id;
$args = array(
'post_id' => $id,
'approve' => 'approve',
'type' => 'order_note'
);
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments', 10, 1 ) );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$order_notes = array();
foreach ( $notes as $note ) {
$order_notes[] = array(
'id' => $note->comment_ID,
'created_at' => $this->server->format_datetime( $note->comment_date_gmt ),
'note' => $note->comment_content,
'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false,
);
}
return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $id, $fields, $notes, $this->server ) );
}
/**
* Helper method to get order post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_orders( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'shop_order',
'post_status' => 'publish',
);
// add status argument
if ( ! empty( $args['status'] ) ) {
$statuses = explode( ',', $args['status'] );
$query_args['tax_query'] = array(
array(
'taxonomy' => 'shop_order_status',
'field' => 'slug',
'terms' => $statuses,
),
);
unset( $args['status'] );
}
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
}

View File

@ -0,0 +1,541 @@
<?php
/**
* WooCommerce API Products Class
*
* Handles requests to the /products endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Products extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/products';
/**
* Register the routes for this class
*
* GET /products
* GET /products/count
* GET|PUT|DELETE /products/<id>
* GET /products/<id>/reviews
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /products
$routes[ $this->base ] = array(
array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
);
# GET /products/count
$routes[ $this->base . '/count'] = array(
array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
);
# GET|PUT|DELETE /products/<id>
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
);
# GET /products/<id>/reviews
$routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get all products
*
* @since 2.1
* @param string $fields
* @param string $type
* @param array $filter
* @param int $page
* @return array
*/
public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {
if ( ! empty( $type ) )
$filter['type'] = $type;
$filter['page'] = $page;
$query = $this->query_products( $filter );
$products = array();
foreach( $query->posts as $product_id ) {
if ( ! $this->is_readable( $product_id ) )
continue;
$products[] = $this->get_product( $product_id, $fields );
}
$this->server->add_pagination_headers( $query );
return array( 'products' => $products );
}
/**
* Get the product for the given ID
*
* @since 2.1
* @param int $id the product ID
* @param string $fields
* @return array
*/
public function get_product( $id, $fields = null ) {
$id = $this->validate_request( $id, 'product', 'read' );
if ( is_wp_error( $id ) )
return $id;
$product = get_product( $id );
// add data that applies to every product type
$product_data = $this->get_product_data( $product );
// add variations to variable products
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
$product_data['variations'] = $this->get_variation_data( $product );
}
// add the parent product data to an individual variation
if ( $product->is_type( 'variation' ) ) {
$product_data['parent'] = $this->get_product_data( $product->parent );
}
return apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server );
}
/**
* Get the total number of orders
*
* @since 2.1
* @param string $type
* @param array $filter
* @return array
*/
public function get_products_count( $type = null, $filter = array() ) {
if ( ! empty( $type ) )
$filter['type'] = $type;
// TODO: permissions?
$query = $this->query_products( $filter );
return array( 'count' => $query->found_posts );
}
/**
* Edit a product
*
* @since 2.1
* @param int $id the product ID
* @param array $data
* @return array
*/
public function edit_product( $id, $data ) {
$id = $this->validate_request( $id, 'product', 'edit' );
if ( is_wp_error( $id ) )
return $id;
// TODO: implement
return $this->get_product( $id );
}
/**
* Delete a product
*
* @since 2.1
* @param int $id the product ID
* @param bool $force true to permanently delete order, false to move to trash
* @return array
*/
public function delete_product( $id, $force = false ) {
$id = $this->validate_request( $id, 'product', 'delete' );
if ( is_wp_error( $id ) )
return $id;
return $this->delete( $id, 'product', ( 'true' === $force ) );
}
/**
* Get the reviews for a product
*
* @since 2.1
* @param int $id the product ID to get reviews for
* @param string $fields fields to include in response
* @return array
*/
public function get_product_reviews( $id, $fields = null ) {
$id = $this->validate_request( $id, 'product', 'read' );
if ( is_wp_error( $id ) )
return $id;
$args = array(
'post_id' => $id,
'approve' => 'approve',
);
$comments = get_comments( $args );
$reviews = array();
foreach ( $comments as $comment ) {
$reviews[] = array(
'id' => $comment->comment_ID,
'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ),
'review' => $comment->comment_content,
'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ),
'reviewer_name' => $comment->comment_author,
'reviewer_email' => $comment->comment_author_email,
'verified' => (bool) woocommerce_customer_bought_product( $comment->comment_author_email, $comment->user_id, $id ),
);
}
return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
}
/**
* Helper method to get product post objects
*
* @since 2.1
* @param array $args request arguments for filtering query
* @return WP_Query
*/
private function query_products( $args ) {
// set base query arguments
$query_args = array(
'fields' => 'ids',
'post_type' => 'product',
'post_status' => 'publish',
'post_parent' => 0,
'meta_query' => array(),
);
if ( ! empty( $args['type'] ) ) {
$types = explode( ',', $args['type'] );
$query_args['tax_query'] = array(
array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $types,
),
);
unset( $args['type'] );
}
$query_args = $this->merge_query_args( $query_args, $args );
return new WP_Query( $query_args );
}
/**
* Get standard product data that applies to every product type
*
* @since 2.1
* @param WC_Product $product
* @return array
*/
private function get_product_data( $product ) {
return array(
'title' => $product->get_title(),
'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id,
'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ),
'type' => $product->product_type,
'status' => $product->get_post_data()->post_status,
'downloadable' => $product->is_downloadable(),
'virtual' => $product->is_virtual(),
'permalink' => $product->get_permalink(),
'sku' => $product->get_sku(),
'price' => woocommerce_format_decimal( $product->get_price() ),
'regular_price' => woocommerce_format_decimal( $product->get_regular_price() ),
'sale_price' => $product->get_sale_price() ? woocommerce_format_decimal( $product->get_sale_price() ) : null,
'price_html' => $product->get_price_html(),
'taxable' => $product->is_taxable(),
'tax_status' => $product->get_tax_status(),
'tax_class' => $product->get_tax_class(),
'managing_stock' => $product->managing_stock(),
'stock_quantity' => (int) $product->get_stock_quantity(),
'in_stock' => $product->is_in_stock(),
'backorders_allowed' => $product->backorders_allowed(),
'backordered' => $product->is_on_backorder(),
'sold_individually' => $product->is_sold_individually(),
'purchaseable' => $product->is_purchasable(),
'featured' => $product->is_featured(),
'visible' => $product->is_visible(),
'catalog_visibility' => $product->visibility,
'on_sale' => $product->is_on_sale(),
'weight' => $product->get_weight() ? woocommerce_format_decimal( $product->get_weight() ) : null,
'dimensions' => array(
'length' => $product->length,
'width' => $product->width,
'height' => $product->height,
'unit' => get_option( 'woocommerce_dimension_unit' ),
),
'shipping_required' => $product->needs_shipping(),
'shipping_taxable' => $product->is_shipping_taxable(),
'shipping_class' => $product->get_shipping_class(),
'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
'description' => apply_filters( 'the_content', $product->get_post_data()->post_content ),
'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ),
'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ),
'average_rating' => woocommerce_format_decimal( $product->get_average_rating() ),
'rating_count' => (int) $product->get_rating_count(),
'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ),
'upsell_ids' => array_map( 'absint', $product->get_upsells() ),
'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ),
'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ),
'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ),
'images' => $this->get_images( $product ),
'attributes' => $this->get_attributes( $product ),
'downloads' => $this->get_downloads( $product ),
'download_limit' => (int) $product->download_limit,
'download_expiry' => (int) $product->download_expiry,
'download_type' => $product->download_type,
'purchase_note' => apply_filters( 'the_content', $product->purchase_note ),
'variations' => array(),
'parent' => array(),
);
}
/**
* Get an individual variation's data
*
* @since 2.1
* @param WC_Product $product
* @return array
*/
private function get_variation_data( $product ) {
$variations = array();
foreach ( $product->get_children() as $child_id ) {
$variation = $product->get_child( $child_id );
if ( ! $variation->exists() )
continue;
$variations[] = array(
'id' => $variation->get_variation_id(),
'created_at' => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ),
'downloadable' => $variation->is_downloadable(),
'virtual' => $variation->is_virtual(),
'permalink' => $variation->get_permalink(),
'sku' => $variation->get_sku(),
'price' => woocommerce_format_decimal( $variation->get_price() ),
'regular_price' => woocommerce_format_decimal( $variation->get_regular_price() ),
'sale_price' => $variation->get_sale_price() ? woocommerce_format_decimal( $variation->get_sale_price() ) : null,
'taxable' => $variation->is_taxable(),
'tax_status' => $variation->get_tax_status(),
'tax_class' => $variation->get_tax_class(),
'stock_quantity' => (int) $variation->get_stock_quantity(),
'in_stock' => $variation->is_in_stock(),
'backordered' => $variation->is_on_backorder(),
'purchaseable' => $variation->is_purchasable(),
'visible' => $variation->variation_is_visible(),
'on_sale' => $variation->is_on_sale(),
'weight' => $variation->get_weight() ? woocommerce_format_decimal( $variation->get_weight() ) : null,
'dimensions' => array(
'length' => $variation->length,
'width' => $variation->width,
'height' => $variation->height,
'unit' => get_option( 'woocommerce_dimension_unit' ),
),
'shipping_class' => $variation->get_shipping_class(),
'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
'image' => $this->get_images( $variation ),
'attributes' => $this->get_attributes( $variation ),
'downloads' => $this->get_downloads( $variation ),
'download_limit' => (int) $product->download_limit,
'download_expiry' => (int) $product->download_expiry,
);
}
return $variations;
}
/**
* Get the images for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_images( $product ) {
$images = $attachment_ids = array();
if ( $product->is_type( 'variation' ) ) {
if ( has_post_thumbnail( $product->get_variation_id() ) ) {
// add variation image if set
$attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() );
} elseif ( has_post_thumbnail( $product->id ) ) {
// otherwise use the parent product featured image if set
$attachment_ids[] = get_post_thumbnail_id( $product->id );
}
} else {
// add featured image
if ( has_post_thumbnail( $product->id ) ) {
$attachment_ids[] = get_post_thumbnail_id( $product->id );
}
// add gallery images
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() );
}
// build image data
foreach ( $attachment_ids as $position => $attachment_id ) {
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) )
continue;
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) )
continue;
$images[] = array(
'id' => (int) $attachment_id,
'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
'src' => current( $attachment ),
'title' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
'position' => $position,
);
}
// set a placeholder image if the product has no images set
if ( empty( $images ) ) {
$images[] = array(
'id' => 0,
'created_at' => $this->server->format_datetime( time() ), // default to now
'updated_at' => $this->server->format_datetime( time() ),
'src' => woocommerce_placeholder_img_src(),
'title' => __( 'Placeholder', 'woocommerce' ),
'alt' => __( 'Placeholder', 'woocommerce' ),
'position' => 0,
);
}
return $images;
}
/**
* Get the attributes for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_attributes( $product ) {
$attributes = array();
if ( $product->is_type( 'variation' ) ) {
// variation attributes
foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
// taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
$attributes[] = array(
'name' => ucwords( str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ) ),
'option' => $attribute,
);
}
} else {
foreach ( $product->get_attributes() as $attribute ) {
// taxonomy-based attributes are comma-separated, others are pipe (|) separated
if ( $attribute['is_taxonomy'] )
$options = explode( ',', $product->get_attribute( $attribute['name'] ) );
else
$options = explode( '|', $product->get_attribute( $attribute['name'] ) );
$attributes[] = array(
'name' => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ),
'position' => $attribute['position'],
'visible' => (bool) $attribute['is_visible'],
'variation' => (bool) $attribute['is_variation'],
'options' => array_map( 'trim', $options ),
);
}
}
return $attributes;
}
/**
* Get the downloads for a product or product variation
*
* @since 2.1
* @param WC_Product|WC_Product_Variation $product
* @return array
*/
private function get_downloads( $product ) {
$downloads = array();
if ( $product->is_downloadable() ) {
foreach ( $product->get_files() as $file_id => $file ) {
$downloads[] = array(
'id' => $file_id, // do not cast as int as this is a hash
'name' => $file['name'],
'file' => $file['file'],
);
}
}
return $downloads;
}
}

View File

@ -0,0 +1,385 @@
<?php
/**
* WooCommerce API Reports Class
*
* Handles requests to the /reports endpoint
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Reports extends WC_API_Resource {
/** @var string $base the route base */
protected $base = '/reports';
/** @var WC_Admin_Report instance */
private $report;
/**
* Register the routes for this class
*
* GET /reports
* GET /reports/sales
*
* @since 2.1
* @param array $routes
* @return array
*/
public function register_routes( $routes ) {
# GET /reports
$routes[ $this->base ] = array(
array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
);
# GET /reports/sales
$routes[ $this->base . '/sales'] = array(
array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
);
return $routes;
}
/**
* Get a simple listing of available reports
*
* @since 2.1
* @return array
*/
public function get_reports() {
return array( 'reports' => array( 'sales' ) );
}
/**
* Get the sales report
*
* @since 2.1
* @param string $fields fields to include in response
* @param array $filter date filtering
* @return array
*/
public function get_sales_report( $fields = null, $filter = array() ) {
// check user permissions
$check = $this->validate_request();
if ( is_wp_error( $check ) )
return $check;
// set date filtering
$this->setup_report( $filter );
// total sales, taxes, shipping, and order count
$totals = $this->report->get_order_report_data( array(
'data' => array(
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'sales'
),
'_order_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'tax'
),
'_order_shipping_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'shipping_tax'
),
'_order_shipping' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'shipping'
),
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'order_count'
)
),
'filter_range' => true,
) );
// total items ordered
$total_items = absint( $this->report->get_order_report_data( array(
'data' => array(
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty'
)
),
'query_type' => 'get_var',
'filter_range' => true,
) ) );
// total discount used
$total_discount = $this->report->get_order_report_data( array(
'data' => array(
'discount_amount' => array(
'type' => 'order_item_meta',
'order_item_type' => 'coupon',
'function' => 'SUM',
'name' => 'discount_amount'
)
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'coupon',
'operator' => '='
)
),
'query_type' => 'get_var',
'filter_range' => true,
) );
// get order totals grouped by period
$orders = $this->report->get_order_report_data( array(
'data' => array(
'_order_total' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_sales'
),
'_order_shipping' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_shipping'
),
'_order_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_tax'
),
'_order_shipping_tax' => array(
'type' => 'meta',
'function' => 'SUM',
'name' => 'total_shipping_tax'
),
'ID' => array(
'type' => 'post_data',
'function' => 'COUNT',
'name' => 'total_orders',
'distinct' => true,
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date'
),
),
'group_by' => $this->report->group_by_query,
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
// get order item totals grouped by period
$order_items = $this->report->get_order_report_data( array(
'data' => array(
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_count'
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date'
),
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'line_item',
'operator' => '='
)
),
'group_by' => $this->report->group_by_query,
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
// get discount totals grouped by period
$discounts = $this->report->get_order_report_data( array(
'data' => array(
'discount_amount' => array(
'type' => 'order_item_meta',
'order_item_type' => 'coupon',
'function' => 'SUM',
'name' => 'discount_amount'
),
'post_date' => array(
'type' => 'post_data',
'function' => '',
'name' => 'post_date'
),
),
'where' => array(
array(
'key' => 'order_item_type',
'value' => 'coupon',
'operator' => '='
)
),
'group_by' => $this->report->group_by_query . ', order_item_name',
'order_by' => 'post_date ASC',
'query_type' => 'get_results',
'filter_range' => true,
) );
$period_totals = array();
// setup period totals by ensuring each period in the interval has data
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
switch ( $this->report->chart_groupby ) {
case 'day' :
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
break;
case 'month' :
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
break;
}
$period_totals[ $time ] = array(
'sales' => woocommerce_format_decimal( 0.00 ),
'orders' => 0,
'items' => 0,
'tax' => woocommerce_format_decimal( 0.00 ),
'shipping' => woocommerce_format_decimal( 0.00 ),
'discount' => woocommerce_format_decimal( 0.00 ),
);
}
// add total sales, total order count, total tax and total shipping for each period
foreach ( $orders as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) )
continue;
$period_totals[ $time ]['sales'] = woocommerce_format_decimal( $order->total_sales );
$period_totals[ $time ]['orders'] = (int) $order->total_orders;
$period_totals[ $time ]['tax'] = woocommerce_format_decimal( $order->total_tax + $order->total_shipping_tax );
$period_totals[ $time ]['shipping'] = woocommerce_format_decimal( $order->total_shipping );
}
// add total order items for each period
foreach ( $order_items as $order_item ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
if ( ! isset( $period_totals[ $time ] ) )
continue;
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
}
// add total discount for each period
foreach ( $discounts as $discount ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
if ( ! isset( $period_totals[ $time ] ) )
continue;
$period_totals[ $time ]['discount'] = woocommerce_format_decimal( $discount->discount_amount );
}
$sales_data = array(
'sales' => woocommerce_format_decimal( $totals->sales ),
'average' => woocommerce_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ) ),
'orders' => (int) $totals->order_count,
'items' => $total_items,
'tax' => woocommerce_format_decimal( $totals->tax + $totals->shipping_tax ),
'shipping' => woocommerce_format_decimal( $totals->shipping ),
'discount' => is_null( $total_discount ) ? woocommerce_format_decimal( 0.00 ) : woocommerce_format_decimal( $total_discount ),
'totals_grouped_by' => $this->report->chart_groupby,
'totals' => $period_totals,
);
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, 'sales', $fields, $this->report, $this->server ) );
}
/**
* Setup the report object and parse any date filtering
*
* @since 2.1
* @param array $filter date filtering
*/
private function setup_report( $filter ) {
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
$this->report = new WC_Admin_Report();
if ( empty( $filter['period'] ) ) {
// custom date range
$filter['period'] = 'custom';
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
// overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
$_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
$_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
} else {
// default custom range to today
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
}
} else {
// ensure period is valid
if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
$filter['period'] = 'week';
}
// TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
// allow "week" for period instead of "7day"
if ( 'week' === $filter['period'] ) {
$filter['period'] = '7day';
}
}
$this->report->calculate_current_range( $filter['period'] );
}
/**
* Verify that the current user has permission to view reports
*
* @since 2.1
* @see WC_API_Resource::validate_request()
* @param null $id unused
* @param null $type unused
* @param null $context unused
* @return bool true if the request is valid and should be processed, false otherwise
*/
protected function validate_request( $id = null, $type = null, $context = null ) {
if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
} else {
return true;
}
}
}

View File

@ -0,0 +1,399 @@
<?php
/**
* WooCommerce API Resource class
*
* Provides shared functionality for resource-specific API classes
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_Resource {
/** @var WC_API_Server the API server */
protected $server;
/** @var string sub-classes override this to set a resource-specific base route */
protected $base;
/**
* Setup class
*
* @since 2.1
* @param WC_API_Server $server
* @return WC_API_Resource
*/
public function __construct( WC_API_Server $server ) {
$this->server = $server;
// automatically register routes for sub-classes
add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
// remove fields from responses when requests specify certain fields
// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
// still has the fields filtered properly
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
}
}
/**
* Validate the request by checking:
*
* 1) the ID is a valid integer
* 2) the ID returns a valid post object and matches the provided post type
* 3) the current user has the proper permissions to read/edit/delete the post
*
* @since 2.1
* @param string|int $id the post ID
* @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
* @param string $context the context of the request, either `read`, `edit` or `delete`
* @return int|WP_Error valid post ID or WP_Error if any of the checks fails
*/
protected function validate_request( $id, $type, $context ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type )
$resource_name = str_replace( 'shop_', '', $type );
else
$resource_name = $type;
$id = absint( $id );
// validate ID
if ( empty( $id ) )
return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
// only custom post types have per-post type/permission checks
if ( 'customer' !== $type ) {
$post = get_post( $id, ARRAY_A );
// for checking permissions, product variations are the same as the product post type
$post_type = ( 'product_variation' === $post['post_type'] ) ? 'product' : $post['post_type'];
// validate post type
if ( $type !== $post_type )
return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
// validate permissions
switch ( $context ) {
case 'read':
if ( ! $this->is_readable( $post ) )
return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
break;
case 'edit':
if ( ! $this->is_editable( $post ) )
return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
break;
case 'delete':
if ( ! $this->is_deletable( $post ) )
return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
break;
}
}
return $id;
}
/**
* Add common request arguments to argument list before WP_Query is run
*
* @since 2.1
* @param array $base_args required arguments for the query (e.g. `post_type`, etc)
* @param array $request_args arguments provided in the request
* @return array
*/
protected function merge_query_args( $base_args, $request_args ) {
$args = array();
// date
if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
$args['date_query'] = array();
// resources created after specified date
if ( ! empty( $request_args['created_at_min'] ) )
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
// resources created before specified date
if ( ! empty( $request_args['created_at_max'] ) )
$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
// resources updated after specified date
if ( ! empty( $request_args['updated_at_min'] ) )
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
// resources updated before specified date
if ( ! empty( $request_args['updated_at_max'] ) )
$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
}
// search
if ( ! empty( $request_args['q'] ) )
$args['s'] = $request_args['q'];
// resources per response
if ( ! empty( $request_args['limit'] ) )
$args['posts_per_page'] = $request_args['limit'];
// resource offset
if ( ! empty( $request_args['offset'] ) )
$args['offset'] = $request_args['offset'];
// resource page
$args['paged'] = absint( $request_args['page'] );
return array_merge( $base_args, $args );
}
/**
* Add meta to resources when requested by the client. Meta is added as a top-level
* `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
*
* @since 2.1
* @param array $data the resource data
* @param object $resource the resource object (e.g WC_Order)
* @return mixed
*/
public function maybe_add_meta( $data, $resource ) {
if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
// don't attempt to add meta more than once
if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) )
return $data;
// define the top-level property name for the meta
switch ( get_class( $resource ) ) {
case 'WC_Order':
$meta_name = 'order_meta';
break;
case 'WC_Coupon':
$meta_name = 'coupon_meta';
break;
case 'WP_User':
$meta_name = 'customer_meta';
break;
default:
$meta_name = 'product_meta';
break;
}
if ( is_a( $resource, 'WP_User' ) ) {
// customer meta
$meta = (array) get_user_meta( $resource->ID );
} elseif ( is_a( $resource, 'WC_Product_Variation' ) ) {
// product variation meta
$meta = (array) get_post_meta( $resource->get_variation_id() );
} else {
// coupon/order/product meta
$meta = (array) get_post_meta( $resource->id );
}
foreach( $meta as $meta_key => $meta_value ) {
// don't add hidden meta by default
if ( ! is_protected_meta( $meta_key ) ) {
$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
}
}
}
return $data;
}
/**
* Restrict the fields included in the response if the request specified certain only certain fields should be returned
*
* @since 2.1
* @param array $data the response data
* @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
* @param array|string the requested list of fields to include in the response
* @return array response data
*/
public function filter_response_fields( $data, $resource, $fields ) {
if ( empty( $fields ) )
return $data;
$fields = explode( ',', $fields );
$sub_fields = array();
// get sub fields
foreach ( $fields as $field ) {
if ( false !== strpos( $field, '.' ) ) {
list( $name, $value ) = explode( '.', $field );
$sub_fields[ $name ] = $value;
}
}
// iterate through top-level fields
foreach ( $data as $data_field => $data_value ) {
// if a field has sub-fields and the top-level field has sub-fields to filter
if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
// iterate through each sub-field
foreach ( $data_value as $sub_field => $sub_field_value ) {
// remove non-matching sub-fields
if ( ! in_array( $sub_field, $sub_fields ) ) {
unset( $data[ $data_field ][ $sub_field ] );
}
}
} else {
// remove non-matching top-level fields
if ( ! in_array( $data_field, $fields ) ) {
unset( $data[ $data_field ] );
}
}
}
return $data;
}
/**
* Delete a given resource
*
* @since 2.1
* @param int $id the resource ID
* @param string $type the resource post type, or `customer`
* @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
* @return array|WP_Error
*/
protected function delete( $id, $type, $force = false ) {
if ( 'shop_order' === $type || 'shop_coupon' === $type )
$resource_name = str_replace( 'shop_', '', $type );
else
$resource_name = $type;
if ( 'customer' === $type ) {
$result = wp_delete_user( $id );
if ( $result )
return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
else
return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
} else {
// delete order/coupon/product
$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
if ( ! $result )
return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
if ( $force ) {
return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
} else {
$this->server->send_status( '202' );
return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
}
}
}
/**
* Checks if the given post is readable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_readable( $post ) {
return $this->check_permission( $post, 'read' );
}
/**
* Checks if the given post is editable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_editable( $post ) {
return $this->check_permission( $post, 'edit' );
}
/**
* Checks if the given post is deletable by the current user
*
* @since 2.1
* @see WC_API_Resource::check_permission()
* @param WP_Post|int $post
* @return bool
*/
protected function is_deletable( $post ) {
return $this->check_permission( $post, 'delete' );
}
/**
* Checks the permissions for the current user given a post and context
*
* @since 2.1
* @param WP_Post|int $post
* @param string $context the type of permission to check, either `read`, `write`, or `delete`
* @return bool true if the current user has the permissions to perform the context on the post
*/
private function check_permission( $post, $context ) {
if ( ! is_a( $post, 'WP_Post' ) )
$post = get_post( $post, ARRAY_A );
if ( is_null( $post ) )
return false;
$post_type = get_post_type_object( $post['post_type'] );
if ( 'read' === $context )
return current_user_can( $post_type->cap->read_post, $post['ID'] );
elseif ( 'edit' === $context )
return current_user_can( $post_type->cap->edit_post, $post['ID'] );
elseif ( 'delete' === $context )
return current_user_can( $post_type->cap->delete_post, $post['ID'] );
else
return false;
}
}

View File

@ -0,0 +1,740 @@
<?php
/**
* WooCommerce API
*
* Handles REST API requests
*
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
* Many thanks to Ryan McCue and any other contributors!
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
require_once ABSPATH . 'wp-admin/includes/admin.php';
class WC_API_Server {
const METHOD_GET = 1;
const METHOD_POST = 2;
const METHOD_PUT = 4;
const METHOD_PATCH = 8;
const METHOD_DELETE = 16;
const READABLE = 1; // GET
const CREATABLE = 2; // POST
const EDITABLE = 14; // POST | PUT | PATCH
const DELETABLE = 16; // DELETE
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
/**
* Does the endpoint accept a raw request body?
*/
const ACCEPT_RAW_DATA = 64;
/** Does the endpoint accept a request body? (either JSON or XML) */
const ACCEPT_DATA = 128;
/**
* Should we hide this endpoint from the index?
*/
const HIDDEN_ENDPOINT = 256;
/**
* Map of HTTP verbs to constants
* @var array
*/
public static $method_map = array(
'HEAD' => self::METHOD_GET,
'GET' => self::METHOD_GET,
'POST' => self::METHOD_POST,
'PUT' => self::METHOD_PUT,
'PATCH' => self::METHOD_PATCH,
'DELETE' => self::METHOD_DELETE,
);
/**
* Requested path (relative to the API root, wp-json.php)
*
* @var string
*/
public $path = '';
/**
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
*
* @var string
*/
public $method = 'HEAD';
/**
* Request parameters
*
* This acts as an abstraction of the superglobals
* (GET => $_GET, POST => $_POST)
*
* @var array
*/
public $params = array( 'GET' => array(), 'POST' => array() );
/**
* Request headers
*
* @var array
*/
public $headers = array();
/**
* Request files (matches $_FILES)
*
* @var array
*/
public $files = array();
/**
* Request/Response handler, either JSON by default
* or XML if requested by client
*
* @var WC_API_Handler
*/
public $handler;
/**
* Setup class and set request/response handler
*
* @since 2.1
* @param $path
* @return WC_API_Server
*/
public function __construct( $path ) {
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) )
$path = $_SERVER['PATH_INFO'];
else
$path = '/';
}
$this->path = $path;
$this->method = $_SERVER['REQUEST_METHOD'];
$this->params['GET'] = $_GET;
$this->params['POST'] = $_POST;
$this->headers = $this->get_headers( $_SERVER );
$this->files = $_FILES;
// Compatibility for clients that can't use PUT/PATCH/DELETE
if ( isset( $_GET['_method'] ) ) {
$this->method = strtoupper( $_GET['_method'] );
}
// determine type of request/response and load handler, JSON by default
if ( $this->is_json_request() )
$handler_class = 'WC_API_JSON_Handler';
elseif ( $this->is_xml_request() )
$handler_class = 'WC_API_XML_Handler';
else
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
$this->handler = new $handler_class();
}
/**
* Check authentication for the request
*
* @since 2.1
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
*/
public function check_authentication() {
// allow plugins to remove default authentication or add their own authentication
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
// API requests run under the context of the authenticated user
if ( is_a( $user, 'WP_User' ) )
wp_set_current_user( $user->ID );
// WP_Errors are handled in serve_request()
elseif ( ! is_wp_error( $user ) )
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => '500' ) );
return $user;
}
/**
* Convert an error to an array
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behaviour, as it is represented as a
* list in JSON rather than an object/map
*
* @since 2.1
* @param WP_Error $error
* @return array List of associative arrays with code and message keys
*/
protected function error_to_array( $error ) {
$errors = array();
foreach ( (array) $error->errors as $code => $messages ) {
foreach ( (array) $messages as $message ) {
$errors[] = array( 'code' => $code, 'message' => $message );
}
}
return $errors;
}
/**
* Handle serving an API request
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 2.1
* @uses WC_API_Server::dispatch()
*/
public function serve_request() {
do_action( 'woocommerce_api_server_before_serve', $this );
$this->header( 'Content-Type', $this->handler->get_content_type(), true );
// the API is enabled by default
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
$this->send_status( 404 );
echo $this->handler->generate_response( array( array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
return;
}
$result = $this->check_authentication();
// if authorization check was successful, dispatch the request
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch();
}
// handle any dispatch errors
if ( is_wp_error( $result ) ) {
$data = $result->get_error_data();
if ( is_array( $data ) && isset( $data['status'] ) ) {
$this->send_status( $data['status'] );
}
$result = $this->error_to_array( $result );
}
// This is a filter rather than an action, since this is designed to be
// re-entrant if needed
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
if ( ! $served ) {
if ( 'HEAD' === $this->method )
return;
echo $this->handler->generate_response( $result );
}
}
/**
* Retrieve the route map
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 2.1
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
*/
public function get_routes() {
// index added by default
$endpoints = array(
'/' => array( array( $this, 'get_index' ), self::READABLE ),
);
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
// Normalise the endpoints
foreach ( $endpoints as $route => &$handlers ) {
if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
$handlers = array( $handlers );
}
}
return $endpoints;
}
/**
* Match the request to a callback and call it
*
* @since 2.1
* @return mixed The value returned by the callback, or a WP_Error instance
*/
public function dispatch() {
switch ( $this->method ) {
case 'HEAD':
case 'GET':
$method = self::METHOD_GET;
break;
case 'POST':
$method = self::METHOD_POST;
break;
case 'PUT':
$method = self::METHOD_PUT;
break;
case 'PATCH':
$method = self::METHOD_PATCH;
break;
case 'DELETE':
$method = self::METHOD_DELETE;
break;
default:
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method' ), array( 'status' => 400 ) );
}
foreach ( $this->get_routes() as $route => $handlers ) {
foreach ( $handlers as $handler ) {
$callback = $handler[0];
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
if ( !( $supported & $method ) )
continue;
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
if ( !$match )
continue;
if ( ! is_callable( $callback ) )
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
$args = array_merge( $args, $this->params['GET'] );
if ( $method & self::METHOD_POST ) {
$args = array_merge( $args, $this->params['POST'] );
}
if ( $supported & self::ACCEPT_DATA ) {
$data = $this->handler->parse_body( $this->get_raw_data() );
$args = array_merge( $args, array( 'data' => $data ) );
}
elseif ( $supported & self::ACCEPT_RAW_DATA ) {
$data = $this->get_raw_data();
$args = array_merge( $args, array( 'data' => $data ) );
}
$args['_method'] = $method;
$args['_route'] = $route;
$args['_path'] = $this->path;
$args['_headers'] = $this->headers;
$args['_files'] = $this->files;
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
// Allow plugins to halt the request via this filter
if ( is_wp_error( $args ) ) {
return $args;
}
$params = $this->sort_callback_params( $callback, $args );
if ( is_wp_error( $params ) )
return $params;
return call_user_func_array( $callback, $params );
}
}
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) );
}
/**
* Sort parameters by order specified in method declaration
*
* Takes a callback and a list of available params, then filters and sorts
* by the parameters the method actually needs, using the Reflection API
*
* @since 2.1
* @param array $callback the endpoint callback
* @param array $provided the provided request parameters
* @return array
*/
protected function sort_callback_params( $callback, $provided ) {
if ( is_array( $callback ) )
$ref_func = new ReflectionMethod( $callback[0], $callback[1] );
else
$ref_func = new ReflectionFunction( $callback );
$wanted = $ref_func->getParameters();
$ordered_parameters = array();
foreach ( $wanted as $param ) {
if ( isset( $provided[ $param->getName() ] ) ) {
// We have this parameters in the list to choose from
$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
}
elseif ( $param->isDefaultValueAvailable() ) {
// We don't have this parameter, but it's optional
$ordered_parameters[] = $param->getDefaultValue();
}
else {
// We don't have this parameter and it wasn't optional, abort!
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s' ), $param->getName() ), array( 'status' => 400 ) );
}
}
return $ordered_parameters;
}
/**
* Get the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 2.1
* @return array Index entity
*/
public function get_index() {
// General site data
$available = array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'URL' => get_option( 'siteurl' ),
'routes' => array(),
'meta' => array(
'timezone' => $this->get_timezone(),
'currency' => get_woocommerce_currency(),
'money_format' => get_woocommerce_currency_symbol(),
'tax_included' => ( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ),
'weight_unit' => get_option( 'woocommerce_weight_unit' ),
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
'links' => array(
'help' => 'http://docs.woothemes.com/document/woocommerce-rest-api/',
'profile' => 'https://raw.github.com/rmccue/WP-API/master/docs/schema.json', // TODO: update this
),
),
);
// Find the available routes
foreach ( $this->get_routes() as $route => $callbacks ) {
$data = array();
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
$methods = array();
foreach ( self::$method_map as $name => $bitmask ) {
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden
if ( $callback[1] & self::HIDDEN_ENDPOINT )
continue 3;
if ( $callback[1] & $bitmask )
$data['supports'][] = $name;
if ( $callback[1] & self::ACCEPT_DATA )
$data['accepts_data'] = true;
// For non-variable routes, generate links
if ( strpos( $route, '<' ) === false ) {
$data['meta'] = array(
'self' => get_woocommerce_api_url( $route ),
);
}
}
}
$available['routes'][$route] = apply_filters( 'woocommerce_api_endpoints_description', $data );
}
return apply_filters( 'woocommerce_api_index', $available );
}
/**
* Send a HTTP status code
*
* @since 2.1
* @param int $code HTTP status
*/
public function send_status( $code ) {
status_header( $code );
}
/**
* Send a HTTP header
*
* @since 2.1
* @param string $key Header key
* @param string $value Header value
* @param boolean $replace Should we replace the existing header?
*/
public function header( $key, $value, $replace = true ) {
header( sprintf( '%s: %s', $key, $value ), $replace );
}
/**
* Send a Link header
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple
*
* @link http://tools.ietf.org/html/rfc5988
* @link http://www.iana.org/assignments/link-relations/link-relations.xml
*
* @since 2.1
* @param string $rel Link relation. Either a registered type, or an absolute URL
* @param string $link Target IRI for the link
* @param array $other Other parameters to send, as an assocative array
*/
public function link_header( $rel, $link, $other = array() ) {
$header = 'Link: <' . $link . '>; rel="' . $rel . '"';
foreach ( $other as $key => $value ) {
if ( 'title' == $key )
$value = '"' . $value . '"';
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Send pagination headers for resources
*
* @since 2.1
* @param WP_Query|WP_User_Query $query
*/
public function add_pagination_headers( $query ) {
// WP_User_Query
if ( is_a( $query, 'WP_User_Query' ) ) {
$page = $query->page;
$single = count( $query->get_results() ) > 1;
$total = $query->get_total();
$total_pages = $query->total_pages;
// WP_Query
} else {
$page = $query->get( 'paged' );
$single = $query->is_single();
$total = $query->found_posts * $query->max_num_pages;
$total_pages = $query->max_num_pages;
}
if ( ! $page )
$page = 1;
$next_page = absint( $page ) + 1;
if ( ! $single ) {
// first/prev
if ( $page > 1 ) {
$this->link_header( 'first', $this->get_paginated_url( 1 ) );
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
}
// next
if ( $next_page <= $total_pages ) {
$this->link_header( 'next', $this->get_paginated_url( $next_page ) );
}
// last
if ( $page != $total_pages )
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
}
$this->header( 'X-WC-Total', $total );
$this->header( 'X-WC-TotalPages', $total_pages );
do_action( 'woocommerce_api_pagination_headers', $this, $query );
}
/**
* Returns the request URL with the page query parmeter set to the specified page
*
* @since 2.1
* @param int $page
* @return string
*/
private function get_paginated_url( $page ) {
// remove existing page query param
$request = remove_query_arg( 'page' );
// add provided page query param
$request = urldecode( add_query_arg( 'page', $page, $request ) );
// return full URL
return get_woocommerce_api_url( str_replace( '/wc-api/v1/', '', $request ) );
}
/**
* Retrieve the raw request entity (body)
*
* @since 2.1
* @return string
*/
public function get_raw_data() {
global $HTTP_RAW_POST_DATA;
// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
// but we can do it ourself.
if ( !isset( $HTTP_RAW_POST_DATA ) ) {
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}
return $HTTP_RAW_POST_DATA;
}
/**
* Parse an RFC3339 datetime into a MySQl datetime
*
* Invalid dates default to unix epoch
*
* @since 2.1
* @param string $datetime RFC3339 datetime
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
*/
public function parse_datetime( $datetime ) {
// Strip millisecond precision (a full stop followed by one or more digits)
if ( strpos( $datetime, '.' ) !== false ) {
$datetime = preg_replace( '/\.\d+/', '', $datetime );
}
// default timezone to UTC
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
try {
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
} catch ( Exception $e ) {
$datetime = new DateTime( '@0' );
}
return $datetime->format( 'Y-m-d H:i:s' );
}
/**
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime
*
* @since 2.1
* @param int|string $timestamp unix timestamp or MySQL datetime
* @param bool $convert_to_utc
* @return string RFC3339 datetime
*/
public function format_datetime( $timestamp, $convert_to_utc = false ) {
if ( $convert_to_utc ) {
$timezone = new DateTimeZone( woocommerce_timezone_string() );
} else {
$timezone = new DateTimeZone( 'UTC' );
}
try {
if ( is_numeric( $timestamp ) ) {
$date = new DateTime( "@{$timestamp}" );
} else {
$date = new DateTime( $timestamp, $timezone );
}
// convert to UTC by adjusting the time based on the offset of the site's timezone
if ( $convert_to_utc ) {
$date->modify( -1 * $date->getOffset() . ' seconds' );
}
} catch ( Exception $e ) {
$date = new DateTime( '@0' );
}
return $date->format( 'Y-m-d\TH:i:s\Z' );
}
/**
* Extract headers from a PHP-style $_SERVER array
*
* @since 2.1
* @param array $server Associative array similar to $_SERVER
* @return array Headers extracted from the input
*/
public function get_headers($server) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_
$additional = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true);
foreach ($server as $key => $value) {
if ( strpos( $key, 'HTTP_' ) === 0) {
$headers[ substr( $key, 5 ) ] = $value;
}
elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
/**
* Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or
* the HTTP ACCEPT header
*
* @since 2.1
* @return bool
*/
private function is_json_request() {
// check path
if ( false !== stripos( $this->path, '.json' ) )
return true;
// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627
if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] )
return true;
return false;
}
/**
* Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or
* the HTTP ACCEPT header
*
* @since 2.1
* @return bool
*/
private function is_xml_request() {
// check path
if ( false !== stripos( $this->path, '.xml' ) )
return true;
// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376
if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) )
return true;
return false;
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* WooCommerce API
*
* Handles parsing XML request bodies and generating XML responses
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API_XML_Handler implements WC_API_Handler {
/**
* Get the content type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type() {
return 'application/xml; charset=' . get_option( 'blog_charset' );
}
/**
* Parse the raw request body entity
*
* @since 2.1
* @param string $body the raw request body
* @return array
*/
public function parse_body( $data ) {
// TODO: implement simpleXML parsing
}
/**
* Generate an XML response given an array of data
*
* @since 2.1
* @param array $data the response data
* @return string
*/
public function generate_response( $data ) {
// TODO: implement array to XML
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* WooCommerce API
*
* Defines an interface that API request/response handlers should implement
*
* @author WooThemes
* @category API
* @package WooCommerce/API
* @since 2.1
*/
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
interface WC_API_Handler {
/**
* Get the content type for the response
*
* This should return the proper HTTP content-type for the response
*
* @since 2.1
* @return string
*/
public function get_content_type();
/**
* Parse the raw request body entity into an array
*
* @since 2.1
* @param string $data
* @return array
*/
public function parse_body( $data );
/**
* Generate a response from an array of data
*
* @since 2.1
* @param array $data
* @return string
*/
public function generate_response( $data );
}

View File

@ -2,36 +2,56 @@
/** /**
* WooCommerce API * WooCommerce API
* *
* This API class handles the WC-API endpoint requests. * Handles WC-API endpoint requests
* *
* @class WC_API * @author WooThemes
* @version 2.0.0 * @category API
* @package WooCommerce/Classes * @package WooCommerce/API
* @category Class * @since 2.0
* @author WooThemes
*/ */
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
class WC_API { class WC_API {
/** This is the major version for the REST API and takes
* first-order position in endpoint URLs
*/
const VERSION = 1;
/** @var WC_API_Server the REST API server */
public $server;
/** /**
* __construct function. * Setup class
* *
* @access public * @access public
* @return void * @since 2.0
* @return WC_API
*/ */
public function __construct() { public function __construct() {
// add query vars
add_filter( 'query_vars', array( $this, 'add_query_vars'), 0 ); add_filter( 'query_vars', array( $this, 'add_query_vars'), 0 );
// register API endpoints
add_action( 'init', array( $this, 'add_endpoint'), 0 ); add_action( 'init', array( $this, 'add_endpoint'), 0 );
add_action( 'parse_request', array( $this, 'api_requests'), 0 );
// handle REST/legacy API request
add_action( 'parse_request', array( $this, 'handle_api_requests'), 0 );
} }
/** /**
* add_query_vars function. * add_query_vars function.
* *
* @access public * @access public
* @return void * @since 2.0
* @param $vars
* @return array
*/ */
public function add_query_vars( $vars ) { public function add_query_vars( $vars ) {
$vars[] = 'wc-api'; $vars[] = 'wc-api';
$vars[] = 'wc-api-route';
return $vars; return $vars;
} }
@ -39,25 +59,58 @@ class WC_API {
* add_endpoint function. * add_endpoint function.
* *
* @access public * @access public
* @since 2.0
* @return void * @return void
*/ */
public function add_endpoint() { public function add_endpoint() {
// REST API
add_rewrite_rule( '^wc-api\/v' . self::VERSION . '/?$', 'index.php?wc-api-route=/', 'top' );
add_rewrite_rule( '^wc-api\/v' . self::VERSION .'(.*)?', 'index.php?wc-api-route=$matches[1]', 'top' );
// legacy API for payment gateway IPNs
add_rewrite_endpoint( 'wc-api', EP_ALL ); add_rewrite_endpoint( 'wc-api', EP_ALL );
} }
/** /**
* API request - Trigger any API requests (handy for third party plugins/gateways). * API request - Trigger any API requests
* *
* @access public * @access public
* @since 2.0
* @return void * @return void
*/ */
public function api_requests() { public function handle_api_requests() {
global $wp; global $wp;
if ( ! empty( $_GET['wc-api'] ) ) if ( ! empty( $_GET['wc-api'] ) )
$wp->query_vars['wc-api'] = $_GET['wc-api']; $wp->query_vars['wc-api'] = $_GET['wc-api'];
if ( ! empty( $_GET['wc-api-route'] ) )
$wp->query_vars['wc-api-route'] = $_GET['wc-api-route'];
// REST API request
if ( ! empty( $wp->query_vars['wc-api-route'] ) ) {
define( 'WC_API_REQUEST', true );
// load required files
$this->includes();
$this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] );
// load API resource classes
$this->register_resources( $this->server );
// Fire off the request
$this->server->serve_request();
exit;
}
// legacy API requests
if ( ! empty( $wp->query_vars['wc-api'] ) ) { 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(); ob_start();
@ -76,4 +129,56 @@ class WC_API {
die('1'); die('1');
} }
} }
}
/**
* Include required files for REST API request
*
* @since 2.1
*/
private function includes() {
// TODO: are all these required?
include_once( ABSPATH . WPINC . '/class-IXR.php' );
include_once( ABSPATH . WPINC . '/class-wp-xmlrpc-server.php' );
include_once( 'api/class-wc-api-server.php' );
include_once( 'api/interface-wc-api-handler.php' );
include_once( 'api/class-wc-api-json-handler.php' );
include_once( 'api/class-wc-api-xml-handler.php' );
include_once( 'api/class-wc-api-authentication.php' );
$this->authentication = new WC_API_Authentication();
include_once( 'api/class-wc-api-resource.php' );
include_once( 'api/class-wc-api-orders.php' );
include_once( 'api/class-wc-api-products.php' );
include_once( 'api/class-wc-api-coupons.php' );
include_once( 'api/class-wc-api-customers.php' );
include_once( 'api/class-wc-api-reports.php' );
// TODO: some action to allow actors to load additional resource types or handlers
}
/**
* Register API resources available
*
* @since 2.1
* @param object $server the REST server
*/
public function register_resources( $server ) {
$api_classes = apply_filters( 'woocommerce_api_classes', array(
'WC_API_Customers',
'WC_API_Orders',
'WC_API_Products',
'WC_API_Coupons',
'WC_API_Reports',
) );
foreach ( $api_classes as $api_class ) {
$this->$api_class = new $api_class( $server );
}
}
}

View File

@ -28,7 +28,7 @@ class WC_Comments {
add_action( 'edit_comment', array( $this, 'clear_transients' ) ); add_action( 'edit_comment', array( $this, 'clear_transients' ) );
// Secure order notes // Secure order notes
add_filter( 'comments_clauses', array( $this, 'exclude_order_comments' ), 10, 1); add_filter( 'comments_clauses', array( __CLASS__, 'exclude_order_comments' ), 10, 1 );
add_action( 'comment_feed_join', array( $this, 'exclude_order_comments_from_feed_join' ) ); add_action( 'comment_feed_join', array( $this, 'exclude_order_comments_from_feed_join' ) );
add_action( 'comment_feed_where', array( $this, 'exclude_order_comments_from_feed_where' ) ); add_action( 'comment_feed_where', array( $this, 'exclude_order_comments_from_feed_where' ) );
} }
@ -40,12 +40,12 @@ class WC_Comments {
* and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and * and are not filtered, however, the code current_user_can( 'read_post', $comment->comment_post_ID ) should keep them safe since only admin and
* shop managers can view orders anyway. * shop managers can view orders anyway.
* *
* The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments') ); * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 );
* *
* @param array $clauses * @param array $clauses
* @return array * @return array
*/ */
public function exclude_order_comments( $clauses ) { public static function exclude_order_comments( $clauses ) {
global $wpdb, $typenow, $pagenow; global $wpdb, $typenow, $pagenow;
if ( is_admin() && $typenow == 'shop_order' && current_user_can( 'manage_woocommerce' ) ) if ( is_admin() && $typenow == 'shop_order' && current_user_can( 'manage_woocommerce' ) )
@ -149,4 +149,4 @@ class WC_Comments {
} }
} }
new WC_Comments(); new WC_Comments();

View File

@ -962,4 +962,4 @@ class WC_Shortcodes {
return ob_get_clean(); return ob_get_clean();
} }
} }

View File

@ -325,7 +325,7 @@ function wc_print_js() {
/** /**
* Set a cookie - wrapper for setcookie using WP constants * Set a cookie - wrapper for setcookie using WP constants
* *
* @param string $name Name of the cookie being set * @param string $name Name of the cookie being set
* @param string $value Value of the cookie * @param string $value Value of the cookie
* @param integer $expire Expiry of the cookie * @param integer $expire Expiry of the cookie
@ -336,4 +336,21 @@ function wc_setcookie( $name, $value, $expire = 0 ) {
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { } elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
trigger_error( "Cookie cannot be set - headers already sent", E_USER_NOTICE ); trigger_error( "Cookie cannot be set - headers already sent", E_USER_NOTICE );
} }
} }
/**
* Get the URL to the WooCommerce REST API
*
* @since 2.1
* @param string $path an endpoint to include in the URL
* @return string the URL
*/
function get_woocommerce_api_url( $path ) {
$url = get_home_url( null, 'wc-api/v' . WC_API::VERSION . '/', ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) ? 'https' : 'http' );
if ( ! empty( $path ) && is_string( $path ) )
$url .= ltrim( $path, '/' );
return $url;
}

View File

@ -380,6 +380,51 @@ function woocommerce_time_format() {
return apply_filters( 'woocommerce_time_format', get_option( 'time_format' ) ); return apply_filters( 'woocommerce_time_format', get_option( 'time_format' ) );
} }
/**
* WooCommerce Timezone - helper to retrieve the timezone string for a site until
* a WP core method exists (see http://core.trac.wordpress.org/ticket/24730)
*
* Adapted from http://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155
*
* @since 2.1
* @access public
* @return string a valid PHP timezone string for the site
*/
function woocommerce_timezone_string() {
// if site timezone string exists, return it
if ( $timezone = get_option( 'timezone_string' ) )
return $timezone;
// get UTC offset, if it isn't set then return UTC
if ( 0 === ( $utc_offset = get_option( 'gmt_offset', 0 ) ) )
return 'UTC';
// adjust UTC offset from hours to seconds
$utc_offset *= 3600;
// attempt to guess the timezone string from the UTC offset
$timezone = timezone_name_from_abbr( '', $utc_offset );
// last try, guess timezone string manually
if ( false === $timezone ) {
$is_dst = date( 'I' );
foreach ( timezone_abbreviations_list() as $abbr ) {
foreach ( $abbr as $city ) {
if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
return $city['timezone_id'];
}
}
}
}
// fallback to UTC
return 'UTC';
}
if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) {
/** /**
@ -534,4 +579,4 @@ function wc_format_postcode( $postcode, $country ) {
function wc_format_phone_number( $tel ) { function wc_format_phone_number( $tel ) {
$tel = str_replace( '.', '-', $tel ); $tel = str_replace( '.', '-', $tel );
return $tel; return $tel;
} }