Merge pull request #4055 from maxrice/api-dev
REST API - Initial Implementation
This commit is contained in:
commit
c720cc2162
|
@ -26,6 +26,12 @@ class WC_Admin_Profile {
|
|||
|
||||
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( '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 ] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' ); ?> </strong><code id="woocommerce_api_consumer_key"><?php echo $user->woocommerce_api_consumer_key ?></code><br/>
|
||||
<strong><?php _e( 'Consumer Secret:', 'woocommerce' ); ?> </strong><code id="woocommerce_api_consumer_secret"><?php echo $user->woocommerce_api_consumer_secret; ?></code><br/>
|
||||
<strong><?php _e( 'Permissions:', 'woocommerce' ); ?> </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;
|
||||
|
||||
return new WC_Admin_Profile();
|
||||
return new WC_Admin_Profile();
|
||||
|
|
|
@ -770,4 +770,4 @@ class WC_Admin_Settings {
|
|||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
endif;
|
||||
|
|
|
@ -99,6 +99,14 @@ class WC_Settings_General extends WC_Settings_Page {
|
|||
'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( '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;
|
||||
|
||||
return new WC_Settings_General();
|
||||
return new WC_Settings_General();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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' );
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
|
||||
}
|
|
@ -2,36 +2,56 @@
|
|||
/**
|
||||
* WooCommerce API
|
||||
*
|
||||
* This API class handles the WC-API endpoint requests.
|
||||
* Handles WC-API endpoint requests
|
||||
*
|
||||
* @class WC_API
|
||||
* @version 2.0.0
|
||||
* @package WooCommerce/Classes
|
||||
* @category Class
|
||||
* @author WooThemes
|
||||
* @author WooThemes
|
||||
* @category API
|
||||
* @package WooCommerce/API
|
||||
* @since 2.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
|
||||
|
||||
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
|
||||
* @return void
|
||||
* @since 2.0
|
||||
* @return WC_API
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// add query vars
|
||||
add_filter( 'query_vars', array( $this, 'add_query_vars'), 0 );
|
||||
|
||||
// register API endpoints
|
||||
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.
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
* @since 2.0
|
||||
* @param $vars
|
||||
* @return array
|
||||
*/
|
||||
public function add_query_vars( $vars ) {
|
||||
$vars[] = 'wc-api';
|
||||
$vars[] = 'wc-api-route';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
|
@ -39,25 +59,58 @@ class WC_API {
|
|||
* add_endpoint function.
|
||||
*
|
||||
* @access public
|
||||
* @since 2.0
|
||||
* @return void
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* API request - Trigger any API requests (handy for third party plugins/gateways).
|
||||
* API request - Trigger any API requests
|
||||
*
|
||||
* @access public
|
||||
* @since 2.0
|
||||
* @return void
|
||||
*/
|
||||
public function api_requests() {
|
||||
public function handle_api_requests() {
|
||||
global $wp;
|
||||
|
||||
if ( ! empty( $_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'] ) ) {
|
||||
|
||||
// Buffer, we won't want any output here
|
||||
ob_start();
|
||||
|
||||
|
@ -76,4 +129,56 @@ class WC_API {
|
|||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class WC_Comments {
|
|||
add_action( 'edit_comment', array( $this, 'clear_transients' ) );
|
||||
|
||||
// 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_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
|
||||
* 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
|
||||
* @return array
|
||||
*/
|
||||
public function exclude_order_comments( $clauses ) {
|
||||
public static function exclude_order_comments( $clauses ) {
|
||||
global $wpdb, $typenow, $pagenow;
|
||||
|
||||
if ( is_admin() && $typenow == 'shop_order' && current_user_can( 'manage_woocommerce' ) )
|
||||
|
@ -149,4 +149,4 @@ class WC_Comments {
|
|||
}
|
||||
}
|
||||
|
||||
new WC_Comments();
|
||||
new WC_Comments();
|
||||
|
|
|
@ -13,7 +13,7 @@ class WC_Shortcodes {
|
|||
/**
|
||||
* Init shortcodes
|
||||
*/
|
||||
public function init() {
|
||||
public static function init() {
|
||||
// Define shortcodes
|
||||
$shortcodes = array(
|
||||
'product' => __CLASS__ . '::product',
|
||||
|
@ -956,4 +956,4 @@ class WC_Shortcodes {
|
|||
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -323,7 +323,7 @@ function wc_print_js() {
|
|||
|
||||
/**
|
||||
* Set a cookie - wrapper for setcookie using WP constants
|
||||
*
|
||||
*
|
||||
* @param string $name Name of the cookie being set
|
||||
* @param string $value Value of the cookie
|
||||
* @param integer $expire Expiry of the cookie
|
||||
|
@ -334,4 +334,21 @@ function wc_setcookie( $name, $value, $expire = 0 ) {
|
|||
} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -363,6 +363,51 @@ function woocommerce_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' ) ) {
|
||||
|
||||
/**
|
||||
|
@ -517,4 +562,4 @@ function wc_format_postcode( $postcode, $country ) {
|
|||
function wc_format_phone_number( $tel ) {
|
||||
$tel = str_replace( '.', '-', $tel );
|
||||
return $tel;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue