Version off the current api files into a v2 folder.
This commit is contained in:
parent
1e5d508675
commit
c9a2611d33
|
@ -0,0 +1,387 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce API Authentication Class
|
||||||
|
*
|
||||||
|
* @author WooThemes
|
||||||
|
* @category API
|
||||||
|
* @package WooCommerce/API
|
||||||
|
* @since 2.1.0
|
||||||
|
* @version 2.4.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly
|
||||||
|
}
|
||||||
|
|
||||||
|
class WC_API_Authentication {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup class
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @return WC_API_Authentication
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
|
||||||
|
// To disable authentication, hook into this filter at a later priority and return a valid WP_User
|
||||||
|
add_filter( 'woocommerce_api_check_authentication', array( $this, 'authenticate' ), 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() ) {
|
||||||
|
$keys = $this->perform_ssl_authentication();
|
||||||
|
} else {
|
||||||
|
$keys = $this->perform_oauth_authentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check API key-specific permission
|
||||||
|
$this->check_api_key_permissions( $keys['permissions'] );
|
||||||
|
|
||||||
|
$user = $this->get_user_by_id( $keys['user_id'] );
|
||||||
|
|
||||||
|
} 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 array
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function perform_ssl_authentication() {
|
||||||
|
|
||||||
|
$params = WC()->api->server->params['GET'];
|
||||||
|
|
||||||
|
// Get consumer key
|
||||||
|
if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
|
||||||
|
|
||||||
|
// Should be in HTTP Auth header by default
|
||||||
|
$consumer_key = $_SERVER['PHP_AUTH_USER'];
|
||||||
|
|
||||||
|
} elseif ( ! empty( $params['consumer_key'] ) ) {
|
||||||
|
|
||||||
|
// Allow a query string parameter as a fallback
|
||||||
|
$consumer_key = $params['consumer_key'];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
throw new Exception( __( 'Consumer Key is missing', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get consumer secret
|
||||||
|
if ( ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
|
||||||
|
|
||||||
|
// Should be in HTTP Auth header by default
|
||||||
|
$consumer_secret = $_SERVER['PHP_AUTH_PW'];
|
||||||
|
|
||||||
|
} elseif ( ! empty( $params['consumer_secret'] ) ) {
|
||||||
|
|
||||||
|
// Allow a query string parameter as a fallback
|
||||||
|
$consumer_secret = $params['consumer_secret'];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
throw new Exception( __( 'Consumer Secret is missing', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = $this->get_keys_by_consumer_key( $consumer_key );
|
||||||
|
|
||||||
|
if ( ! $this->is_consumer_secret_valid( $keys['consumer_secret'], $consumer_secret ) ) {
|
||||||
|
throw new Exception( __( 'Consumer Secret is invalid', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests
|
||||||
|
*
|
||||||
|
* This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP
|
||||||
|
*
|
||||||
|
* This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
|
||||||
|
*
|
||||||
|
* 1) There is no token associated with request/responses, only consumer keys/secrets are used
|
||||||
|
*
|
||||||
|
* 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
|
||||||
|
* This is because there is no cross-OS function within PHP to get the raw Authorization header
|
||||||
|
*
|
||||||
|
* @link http://tools.ietf.org/html/rfc5849 for the full spec
|
||||||
|
* @since 2.1
|
||||||
|
* @return array
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function perform_oauth_authentication() {
|
||||||
|
|
||||||
|
$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[ $param_name ] ) ) {
|
||||||
|
throw new Exception( sprintf( __( '%s parameter is missing', 'woocommerce' ), $param_name ), 404 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch WP user by consumer key
|
||||||
|
$keys = $this->get_keys_by_consumer_key( $params['oauth_consumer_key'] );
|
||||||
|
|
||||||
|
// Perform OAuth validation
|
||||||
|
$this->check_oauth_signature( $keys, $params );
|
||||||
|
$this->check_oauth_timestamp_and_nonce( $keys, $params['oauth_timestamp'], $params['oauth_nonce'] );
|
||||||
|
|
||||||
|
// Authentication successful, return user
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the keys for the given consumer key
|
||||||
|
*
|
||||||
|
* @since 2.4.0
|
||||||
|
* @param string $consumer_key
|
||||||
|
* @return array
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function get_keys_by_consumer_key( $consumer_key ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$consumer_key = wc_api_hash( sanitize_text_field( $consumer_key ) );
|
||||||
|
|
||||||
|
$keys = $wpdb->get_row( $wpdb->prepare( "
|
||||||
|
SELECT *
|
||||||
|
FROM {$wpdb->prefix}woocommerce_api_keys
|
||||||
|
WHERE consumer_key = '%s'
|
||||||
|
", $consumer_key ), ARRAY_A );
|
||||||
|
|
||||||
|
if ( empty( $keys ) ) {
|
||||||
|
throw new Exception( __( 'Consumer Key is invalid', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user by ID
|
||||||
|
*
|
||||||
|
* @since 2.4.0
|
||||||
|
* @param int $user_id
|
||||||
|
* @return WC_User
|
||||||
|
*/
|
||||||
|
private function get_user_by_id( $user_id ) {
|
||||||
|
$user = get_user_by( 'id', $user_id );
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
throw new Exception( __( 'API user is invalid', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the consumer secret provided for the given user is valid
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @param string $keys_consumer_secret
|
||||||
|
* @param string $consumer_secret
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function is_consumer_secret_valid( $keys_consumer_secret, $consumer_secret ) {
|
||||||
|
return hash_equals( $keys_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 array $keys
|
||||||
|
* @param array $params the request parameters
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function check_oauth_signature( $keys, $params ) {
|
||||||
|
|
||||||
|
$http_method = strtoupper( WC()->api->server->method );
|
||||||
|
|
||||||
|
$base_request_uri = rawurlencode( untrailingslashit( get_woocommerce_api_url( '' ) ) . WC()->api->server->path );
|
||||||
|
|
||||||
|
// Get the signature provided by the consumer and remove it from the parameters prior to checking the signature
|
||||||
|
$consumer_signature = rawurldecode( $params['oauth_signature'] );
|
||||||
|
unset( $params['oauth_signature'] );
|
||||||
|
|
||||||
|
// Remove filters and convert them from array to strings to void normalize issues
|
||||||
|
if ( isset( $params['filter'] ) ) {
|
||||||
|
$filters = $params['filter'];
|
||||||
|
unset( $params['filter'] );
|
||||||
|
foreach ( $filters as $filter => $filter_value ) {
|
||||||
|
$params['filter[' . $filter . ']'] = $filter_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize parameter key/values
|
||||||
|
$params = $this->normalize_parameters( $params );
|
||||||
|
|
||||||
|
// 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, $keys['consumer_secret'], true ) );
|
||||||
|
|
||||||
|
if ( ! hash_equals( $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
|
||||||
|
*
|
||||||
|
* Note both the key and value is normalized so a filter param like:
|
||||||
|
*
|
||||||
|
* 'filter[period]' => 'week'
|
||||||
|
*
|
||||||
|
* is encoded to:
|
||||||
|
*
|
||||||
|
* 'filter%5Bperiod%5D' => 'week'
|
||||||
|
*
|
||||||
|
* This conforms to the OAuth 1.0a spec which indicates the entire query string
|
||||||
|
* should be URL encoded
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @see rawurlencode()
|
||||||
|
* @param array $parameters un-normalized pararmeters
|
||||||
|
* @return array normalized parameters
|
||||||
|
*/
|
||||||
|
private function normalize_parameters( $parameters ) {
|
||||||
|
|
||||||
|
$normalized_parameters = array();
|
||||||
|
|
||||||
|
foreach ( $parameters as $key => $value ) {
|
||||||
|
|
||||||
|
// Percent symbols (%) must be double-encoded
|
||||||
|
$key = str_replace( '%', '%25', rawurlencode( rawurldecode( $key ) ) );
|
||||||
|
$value = str_replace( '%', '%25', rawurlencode( rawurldecode( $value ) ) );
|
||||||
|
|
||||||
|
$normalized_parameters[ $key ] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized_parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
|
||||||
|
* an attacker could attempt to re-send an intercepted request at a later time.
|
||||||
|
*
|
||||||
|
* - A timestamp is valid if it is within 15 minutes of now
|
||||||
|
* - A nonce is valid if it has not been used within the last 15 minutes
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @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( $keys, $timestamp, $nonce ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$valid_window = 15 * 60; // 15 minute window
|
||||||
|
|
||||||
|
if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
|
||||||
|
throw new Exception( __( 'Invalid timestamp', 'woocommerce' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$used_nonces = maybe_unserialize( $keys['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 < ( time() - $valid_window ) ) {
|
||||||
|
unset( $used_nonces[ $nonce_timestamp ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$used_nonces = maybe_serialize( $used_nonces );
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'woocommerce_api_keys',
|
||||||
|
array( 'nonces' => $used_nonces ),
|
||||||
|
array( 'key_id' => $keys['key_id'] ),
|
||||||
|
array( '%s' ),
|
||||||
|
array( '%d' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the API keys provided have the proper key-specific permissions to either read or write API resources
|
||||||
|
*
|
||||||
|
* @param string $key_permissions
|
||||||
|
* @throws Exception if the permission check fails
|
||||||
|
*/
|
||||||
|
public function check_api_key_permissions( $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,572 @@
|
||||||
|
<?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 /coupons
|
||||||
|
* GET /coupons/count
|
||||||
|
* GET /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 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# POST|PUT /coupons/bulk
|
||||||
|
$routes[ $this->base . '/bulk' ] = array(
|
||||||
|
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||||
|
);
|
||||||
|
|
||||||
|
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[] = current( $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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
$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 ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_id', __( 'Invalid coupon ID', 'woocommerce' ), 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' => wc_format_decimal( $coupon->coupon_amount, 2 ),
|
||||||
|
'individual_use' => ( 'yes' === $coupon->individual_use ),
|
||||||
|
'product_ids' => array_map( 'absint', (array) $coupon->product_ids ),
|
||||||
|
'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ),
|
||||||
|
'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null,
|
||||||
|
'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null,
|
||||||
|
'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items,
|
||||||
|
'usage_count' => (int) $coupon->usage_count,
|
||||||
|
'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->server->format_datetime( $coupon->expiry_date ) : null,
|
||||||
|
'enable_free_shipping' => $coupon->enable_free_shipping(),
|
||||||
|
'product_category_ids' => array_map( 'absint', (array) $coupon->product_categories ),
|
||||||
|
'exclude_product_category_ids' => array_map( 'absint', (array) $coupon->exclude_product_categories ),
|
||||||
|
'exclude_sale_items' => $coupon->exclude_sale_items(),
|
||||||
|
'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ),
|
||||||
|
'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ),
|
||||||
|
'customer_emails' => $coupon->customer_email,
|
||||||
|
'description' => $coupon_post->post_excerpt,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array( 'coupon' => apply_filters( 'woocommerce_api_coupon_response', $coupon_data, $coupon, $fields, $this->server ) );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of coupons
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @param array $filter
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_coupons_count( $filter = array() ) {
|
||||||
|
try {
|
||||||
|
if ( ! current_user_can( 'read_private_shop_coupons' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_coupons_count', __( 'You do not have permission to read the coupons count', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->query_coupons( $filter );
|
||||||
|
|
||||||
|
return array( 'count' => (int) $query->found_posts );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$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 ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_code', __( 'Invalid coupon code', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->get_coupon( $id, $fields );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a coupon
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create_coupon( $data ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['coupon'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['coupon'];
|
||||||
|
|
||||||
|
// Check user permission
|
||||||
|
if ( ! current_user_can( 'publish_shop_coupons' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_coupon', __( 'You do not have permission to create coupons', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_create_coupon_data', $data, $this );
|
||||||
|
|
||||||
|
// Check if coupon code is specified
|
||||||
|
if ( ! isset( $data['code'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$coupon_code = apply_filters( 'woocommerce_coupon_code', $data['code'] );
|
||||||
|
|
||||||
|
// Check for duplicate coupon codes
|
||||||
|
$coupon_found = $wpdb->get_var( $wpdb->prepare( "
|
||||||
|
SELECT $wpdb->posts.ID
|
||||||
|
FROM $wpdb->posts
|
||||||
|
WHERE $wpdb->posts.post_type = 'shop_coupon'
|
||||||
|
AND $wpdb->posts.post_status = 'publish'
|
||||||
|
AND $wpdb->posts.post_title = '%s'
|
||||||
|
", $coupon_code ) );
|
||||||
|
|
||||||
|
if ( $coupon_found ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = array(
|
||||||
|
'type' => 'fixed_cart',
|
||||||
|
'amount' => 0,
|
||||||
|
'individual_use' => false,
|
||||||
|
'product_ids' => array(),
|
||||||
|
'exclude_product_ids' => array(),
|
||||||
|
'usage_limit' => '',
|
||||||
|
'usage_limit_per_user' => '',
|
||||||
|
'limit_usage_to_x_items' => '',
|
||||||
|
'usage_count' => '',
|
||||||
|
'expiry_date' => '',
|
||||||
|
'enable_free_shipping' => false,
|
||||||
|
'product_category_ids' => array(),
|
||||||
|
'exclude_product_category_ids' => array(),
|
||||||
|
'exclude_sale_items' => false,
|
||||||
|
'minimum_amount' => '',
|
||||||
|
'maximum_amount' => '',
|
||||||
|
'customer_emails' => array(),
|
||||||
|
'description' => ''
|
||||||
|
);
|
||||||
|
|
||||||
|
$coupon_data = wp_parse_args( $data, $defaults );
|
||||||
|
|
||||||
|
// Validate coupon types
|
||||||
|
if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_coupon = array(
|
||||||
|
'post_title' => $coupon_code,
|
||||||
|
'post_content' => '',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_author' => get_current_user_id(),
|
||||||
|
'post_type' => 'shop_coupon',
|
||||||
|
'post_excerpt' => $coupon_data['description']
|
||||||
|
);
|
||||||
|
|
||||||
|
$id = wp_insert_post( $new_coupon, $wp_error = false );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_cannot_create_coupon', $id->get_error_message(), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set coupon meta
|
||||||
|
update_post_meta( $id, 'discount_type', $coupon_data['type'] );
|
||||||
|
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) );
|
||||||
|
update_post_meta( $id, 'individual_use', ( true === $coupon_data['individual_use'] ) ? 'yes' : 'no' );
|
||||||
|
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) );
|
||||||
|
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) );
|
||||||
|
update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) );
|
||||||
|
update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) );
|
||||||
|
update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) );
|
||||||
|
update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) );
|
||||||
|
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) );
|
||||||
|
update_post_meta( $id, 'free_shipping', ( true === $coupon_data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||||
|
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) );
|
||||||
|
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) );
|
||||||
|
update_post_meta( $id, 'exclude_sale_items', ( true === $coupon_data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||||
|
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) );
|
||||||
|
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) );
|
||||||
|
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) );
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_create_coupon', $id, $data );
|
||||||
|
|
||||||
|
$this->server->send_status( 201 );
|
||||||
|
|
||||||
|
return $this->get_coupon( $id );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a coupon
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id the coupon ID
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function edit_coupon( $id, $data ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['coupon'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_coupon_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'coupon' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['coupon'];
|
||||||
|
|
||||||
|
$id = $this->validate_request( $id, 'shop_coupon', 'edit' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_edit_coupon_data', $data, $id, $this );
|
||||||
|
|
||||||
|
if ( isset( $data['code'] ) ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$coupon_code = apply_filters( 'woocommerce_coupon_code', $data['code'] );
|
||||||
|
|
||||||
|
// Check for duplicate coupon codes
|
||||||
|
$coupon_found = $wpdb->get_var( $wpdb->prepare( "
|
||||||
|
SELECT $wpdb->posts.ID
|
||||||
|
FROM $wpdb->posts
|
||||||
|
WHERE $wpdb->posts.post_type = 'shop_coupon'
|
||||||
|
AND $wpdb->posts.post_status = 'publish'
|
||||||
|
AND $wpdb->posts.post_title = '%s'
|
||||||
|
AND $wpdb->posts.ID != %s
|
||||||
|
", $coupon_code, $id ) );
|
||||||
|
|
||||||
|
if ( $coupon_found ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code, 'post_excerpt' => isset( $data['description'] ) ? $data['description'] : '' ) );
|
||||||
|
if ( 0 === $id ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['type'] ) ) {
|
||||||
|
// Validate coupon types
|
||||||
|
if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ), 400 );
|
||||||
|
}
|
||||||
|
update_post_meta( $id, 'discount_type', $data['type'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['amount'] ) ) {
|
||||||
|
update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['individual_use'] ) ) {
|
||||||
|
update_post_meta( $id, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['product_ids'] ) ) {
|
||||||
|
update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['exclude_product_ids'] ) ) {
|
||||||
|
update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['usage_limit'] ) ) {
|
||||||
|
update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['usage_limit_per_user'] ) ) {
|
||||||
|
update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['limit_usage_to_x_items'] ) ) {
|
||||||
|
update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['usage_count'] ) ) {
|
||||||
|
update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['expiry_date'] ) ) {
|
||||||
|
update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['enable_free_shipping'] ) ) {
|
||||||
|
update_post_meta( $id, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['product_category_ids'] ) ) {
|
||||||
|
update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['exclude_product_category_ids'] ) ) {
|
||||||
|
update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['exclude_sale_items'] ) ) {
|
||||||
|
update_post_meta( $id, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['minimum_amount'] ) ) {
|
||||||
|
update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['maximum_amount'] ) ) {
|
||||||
|
update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $data['customer_emails'] ) ) {
|
||||||
|
update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_edit_coupon', $id, $data );
|
||||||
|
|
||||||
|
return $this->get_coupon( $id );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a coupon
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_delete_coupon', $id, $this );
|
||||||
|
|
||||||
|
return $this->delete( $id, 'shop_coupon', ( 'true' === $force ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* expiry_date format
|
||||||
|
*
|
||||||
|
* @since 2.3.0
|
||||||
|
* @param string $expiry_date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_coupon_expiry_date( $expiry_date ) {
|
||||||
|
if ( '' != $expiry_date ) {
|
||||||
|
return date( 'Y-m-d', strtotime( $expiry_date ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk update or insert coupons
|
||||||
|
* Accepts an array with coupons in the formats supported by
|
||||||
|
* WC_API_Coupons->create_coupon() and WC_API_Coupons->edit_coupon()
|
||||||
|
*
|
||||||
|
* @since 2.4.0
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function bulk( $data ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['coupons'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_coupons_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'coupons' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['coupons'];
|
||||||
|
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'coupons' );
|
||||||
|
|
||||||
|
// Limit bulk operation
|
||||||
|
if ( count( $data ) > $limit ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_coupons_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request', 'woocommerce' ), $limit ), 413 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$coupons = array();
|
||||||
|
|
||||||
|
foreach ( $data as $_coupon ) {
|
||||||
|
$coupon_id = 0;
|
||||||
|
|
||||||
|
// Try to get the coupon ID
|
||||||
|
if ( isset( $_coupon['id'] ) ) {
|
||||||
|
$coupon_id = intval( $_coupon['id'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coupon exists / edit coupon
|
||||||
|
if ( $coupon_id ) {
|
||||||
|
$edit = $this->edit_coupon( $coupon_id, array( 'coupon' => $_coupon ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $edit ) ) {
|
||||||
|
$coupons[] = array(
|
||||||
|
'id' => $coupon_id,
|
||||||
|
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$coupons[] = $edit['coupon'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coupon don't exists / create coupon
|
||||||
|
else {
|
||||||
|
$new = $this->create_coupon( array( 'coupon' => $_coupon ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $new ) ) {
|
||||||
|
$coupons[] = array(
|
||||||
|
'id' => $coupon_id,
|
||||||
|
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$coupons[] = $new['coupon'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'coupons' => apply_filters( 'woocommerce_api_coupons_bulk_response', $coupons, $this ) );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,845 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce API Customers Class
|
||||||
|
*
|
||||||
|
* Handles requests to the /customers endpoint
|
||||||
|
*
|
||||||
|
* @author WooThemes
|
||||||
|
* @category API
|
||||||
|
* @package WooCommerce/API
|
||||||
|
* @since 2.2
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 /customers
|
||||||
|
* GET /customers/count
|
||||||
|
* GET /customers/<id>
|
||||||
|
* GET /customers/<id>/orders
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @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/email/<email>
|
||||||
|
$routes[ $this->base . '/email/(?P<email>.+)' ] = array(
|
||||||
|
array( array( $this, 'get_customer_by_email' ), WC_API_SERVER::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /customers/<id>/orders
|
||||||
|
$routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
|
||||||
|
array( array( $this, 'get_customer_orders' ), WC_API_SERVER::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /customers/<id>/downloads
|
||||||
|
$routes[ $this->base . '/(?P<id>\d+)/downloads' ] = array(
|
||||||
|
array( array( $this, 'get_customer_downloads' ), WC_API_SERVER::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# POST|PUT /customers/bulk
|
||||||
|
$routes[ $this->base . '/bulk' ] = array(
|
||||||
|
array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||||
|
);
|
||||||
|
|
||||||
|
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[] = current( $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 array $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 IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' )
|
||||||
|
ORDER BY posts.ID DESC
|
||||||
|
" );
|
||||||
|
|
||||||
|
$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,
|
||||||
|
'role' => $customer->roles[0],
|
||||||
|
'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' => wc_get_customer_order_count( $customer->ID ),
|
||||||
|
'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ),
|
||||||
|
'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 array( 'customer' => apply_filters( 'woocommerce_api_customer_response', $customer_data, $customer, $fields, $this->server ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the customer for the given email
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @param string $email the customer email
|
||||||
|
* @param array $fields
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_customer_by_email( $email, $fields = null ) {
|
||||||
|
try {
|
||||||
|
if ( is_email( $email ) ) {
|
||||||
|
$customer = get_user_by( 'email', $email );
|
||||||
|
if ( ! is_object( $customer ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer Email', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_email', __( 'Invalid customer Email', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->get_customer( $customer->ID, $fields );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of customers
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @param array $filter
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_customers_count( $filter = array() ) {
|
||||||
|
try {
|
||||||
|
if ( ! current_user_can( 'list_users' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customers_count', __( 'You do not have permission to read the customers count', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->query_customers( $filter );
|
||||||
|
|
||||||
|
return array( 'count' => count( $query->get_results() ) );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get customer billing address fields.
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_customer_billing_address() {
|
||||||
|
$billing_address = apply_filters( 'woocommerce_api_customer_billing_address', array(
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'company',
|
||||||
|
'address_1',
|
||||||
|
'address_2',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'postcode',
|
||||||
|
'country',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
) );
|
||||||
|
|
||||||
|
return $billing_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get customer shipping address fields.
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_customer_shipping_address() {
|
||||||
|
$shipping_address = apply_filters( 'woocommerce_api_customer_shipping_address', array(
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'company',
|
||||||
|
'address_1',
|
||||||
|
'address_2',
|
||||||
|
'city',
|
||||||
|
'state',
|
||||||
|
'postcode',
|
||||||
|
'country',
|
||||||
|
) );
|
||||||
|
|
||||||
|
return $shipping_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add/Update customer data.
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id the customer ID
|
||||||
|
* @param array $data
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function update_customer_data( $id, $data ) {
|
||||||
|
// Customer first name.
|
||||||
|
if ( isset( $data['first_name'] ) ) {
|
||||||
|
update_user_meta( $id, 'first_name', wc_clean( $data['first_name'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer last name.
|
||||||
|
if ( isset( $data['last_name'] ) ) {
|
||||||
|
update_user_meta( $id, 'last_name', wc_clean( $data['last_name'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer billing address.
|
||||||
|
if ( isset( $data['billing_address'] ) ) {
|
||||||
|
foreach ( $this->get_customer_billing_address() as $address ) {
|
||||||
|
if ( isset( $data['billing_address'][ $address ] ) ) {
|
||||||
|
update_user_meta( $id, 'billing_' . $address, wc_clean( $data['billing_address'][ $address ] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer shipping address.
|
||||||
|
if ( isset( $data['shipping_address'] ) ) {
|
||||||
|
foreach ( $this->get_customer_shipping_address() as $address ) {
|
||||||
|
if ( isset( $data['shipping_address'][ $address ] ) ) {
|
||||||
|
update_user_meta( $id, 'shipping_' . $address, wc_clean( $data['shipping_address'][ $address ] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_update_customer_data', $id, $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a customer
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create_customer( $data ) {
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['customer'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['customer'];
|
||||||
|
|
||||||
|
// Checks with can create new users.
|
||||||
|
if ( ! current_user_can( 'create_users' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_customer', __( 'You do not have permission to create this customer', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_create_customer_data', $data, $this );
|
||||||
|
|
||||||
|
// Checks with the email is missing.
|
||||||
|
if ( ! isset( $data['email'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_customer_email', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'email' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the username.
|
||||||
|
$data['username'] = ! empty( $data['username'] ) ? $data['username'] : '';
|
||||||
|
|
||||||
|
// Sets the password.
|
||||||
|
$data['password'] = ! empty( $data['password'] ) ? $data['password'] : '';
|
||||||
|
|
||||||
|
// Attempts to create the new customer
|
||||||
|
$id = wc_create_new_customer( $data['email'], $data['username'], $data['password'] );
|
||||||
|
|
||||||
|
// Checks for an error in the customer creation.
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Added customer data.
|
||||||
|
$this->update_customer_data( $id, $data );
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_create_customer', $id, $data );
|
||||||
|
|
||||||
|
$this->server->send_status( 201 );
|
||||||
|
|
||||||
|
return $this->get_customer( $id );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a customer
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id the customer ID
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function edit_customer( $id, $data ) {
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['customer'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_customer_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'customer' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['customer'];
|
||||||
|
|
||||||
|
// Validate the customer ID.
|
||||||
|
$id = $this->validate_request( $id, 'customer', 'edit' );
|
||||||
|
|
||||||
|
// Return the validate error.
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
throw new WC_API_Exception( $id->get_error_code(), $id->get_error_message(), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_edit_customer_data', $data, $this );
|
||||||
|
|
||||||
|
// Customer email.
|
||||||
|
if ( isset( $data['email'] ) ) {
|
||||||
|
wp_update_user( array( 'ID' => $id, 'user_email' => sanitize_email( $data['email'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer password.
|
||||||
|
if ( isset( $data['password'] ) ) {
|
||||||
|
wp_update_user( array( 'ID' => $id, 'user_pass' => wc_clean( $data['password'] ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update customer data.
|
||||||
|
$this->update_customer_data( $id, $data );
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_edit_customer', $id, $data );
|
||||||
|
|
||||||
|
return $this->get_customer( $id );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a customer
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id the customer ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function delete_customer( $id ) {
|
||||||
|
|
||||||
|
// Validate the customer ID.
|
||||||
|
$id = $this->validate_request( $id, 'customer', 'delete' );
|
||||||
|
|
||||||
|
// Return the validate error.
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_delete_customer', $id, $this );
|
||||||
|
|
||||||
|
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 IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' )
|
||||||
|
", $id ) );
|
||||||
|
|
||||||
|
if ( empty( $order_ids ) ) {
|
||||||
|
return array( 'orders' => array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders = array();
|
||||||
|
|
||||||
|
foreach ( $order_ids as $order_id ) {
|
||||||
|
$orders[] = current( 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 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the available downloads for a customer
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id the customer ID
|
||||||
|
* @param string $fields fields to include in response
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_customer_downloads( $id, $fields = null ) {
|
||||||
|
$id = $this->validate_request( $id, 'customer', 'read' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$downloads = array();
|
||||||
|
$_downloads = wc_get_customer_available_downloads( $id );
|
||||||
|
|
||||||
|
foreach ( $_downloads as $key => $download ) {
|
||||||
|
$downloads[ $key ] = $download;
|
||||||
|
$downloads[ $key ]['access_expires'] = $this->server->format_datetime( $downloads[ $key ]['access_expires'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'downloads' => apply_filters( 'woocommerce_api_customer_downloads_response', $downloads, $id, $fields, $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
|
||||||
|
*
|
||||||
|
* The filter for role can only be a single role in a string.
|
||||||
|
*
|
||||||
|
* @since 2.3
|
||||||
|
* @param array $args request arguments for filtering query
|
||||||
|
* @return WP_User_Query
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom Role
|
||||||
|
if ( ! empty( $args['role'] ) ) {
|
||||||
|
$query_args['role'] = $args['role'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if ( ! empty( $args['q'] ) ) {
|
||||||
|
$query_args['search'] = $args['q'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit number of users returned
|
||||||
|
if ( ! empty( $args['limit'] ) ) {
|
||||||
|
if ( $args['limit'] == -1 ) {
|
||||||
|
unset( $query_args['number'] );
|
||||||
|
} else {
|
||||||
|
$query_args['number'] = absint( $args['limit'] );
|
||||||
|
$users_per_page = absint( $args['limit'] );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$args['limit'] = $query_args['number'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page
|
||||||
|
$page = ( isset( $args['page'] ) ) ? absint( $args['page'] ) : 1;
|
||||||
|
|
||||||
|
// 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'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order (ASC or DESC, ASC by default)
|
||||||
|
if ( ! empty( $args['order'] ) ) {
|
||||||
|
$query_args['order'] = $args['order'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orderby
|
||||||
|
if ( ! empty( $args['orderby'] ) ) {
|
||||||
|
$query_args['orderby'] = $args['orderby'];
|
||||||
|
|
||||||
|
// Allow sorting by meta value
|
||||||
|
if ( ! empty( $args['orderby_meta_key'] ) ) {
|
||||||
|
$query_args['meta_key'] = $args['orderby_meta_key'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new WP_User_Query( $query_args );
|
||||||
|
|
||||||
|
// Helper members for pagination headers
|
||||||
|
$query->total_pages = ( $args['limit'] == -1 ) ? 1 : 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 ) {
|
||||||
|
|
||||||
|
// add customer data from order
|
||||||
|
$order_data['customer'] = array(
|
||||||
|
'id' => 0,
|
||||||
|
'email' => $order->billing_email,
|
||||||
|
'first_name' => $order->billing_first_name,
|
||||||
|
'last_name' => $order->billing_last_name,
|
||||||
|
'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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$order_data['customer'] = current( $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
|
||||||
|
*
|
||||||
|
* Kudos to https://github.com/WP-API/WP-API for offering a better solution
|
||||||
|
*
|
||||||
|
* @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 ) {
|
||||||
|
$avatar_html = get_avatar( $email );
|
||||||
|
|
||||||
|
// Get the URL of the avatar from the provided HTML
|
||||||
|
preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches );
|
||||||
|
|
||||||
|
if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) {
|
||||||
|
return esc_url_raw( $matches[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 integer $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 ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
$id = absint( $id );
|
||||||
|
|
||||||
|
// validate ID
|
||||||
|
if ( empty( $id ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Invalid customer ID', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-existent IDs return a valid WP_User object with the user ID = 0
|
||||||
|
$customer = new WP_User( $id );
|
||||||
|
|
||||||
|
if ( 0 === $customer->ID ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_customer', __( 'Invalid customer', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate permissions
|
||||||
|
switch ( $context ) {
|
||||||
|
|
||||||
|
case 'read':
|
||||||
|
if ( ! current_user_can( 'list_users' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read this customer', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
if ( ! current_user_can( 'edit_users' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_customer', __( 'You do not have permission to edit this customer', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if ( ! current_user_can( 'delete_users' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_customer', __( 'You do not have permission to delete this customer', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk update or insert customers
|
||||||
|
* Accepts an array with customers in the formats supported by
|
||||||
|
* WC_API_Customers->create_customer() and WC_API_Customers->edit_customer()
|
||||||
|
*
|
||||||
|
* @since 2.4.0
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function bulk( $data ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['customers'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_customers_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'customers' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['customers'];
|
||||||
|
$limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'customers' );
|
||||||
|
|
||||||
|
// Limit bulk operation
|
||||||
|
if ( count( $data ) > $limit ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_customers_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request', 'woocommerce' ), $limit ), 413 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$customers = array();
|
||||||
|
|
||||||
|
foreach ( $data as $_customer ) {
|
||||||
|
$customer_id = 0;
|
||||||
|
|
||||||
|
// Try to get the customer ID
|
||||||
|
if ( isset( $_customer['id'] ) ) {
|
||||||
|
$customer_id = intval( $_customer['id'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer exists / edit customer
|
||||||
|
if ( $customer_id ) {
|
||||||
|
$edit = $this->edit_customer( $customer_id, array( 'customer' => $_customer ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $edit ) ) {
|
||||||
|
$customers[] = array(
|
||||||
|
'id' => $customer_id,
|
||||||
|
'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$customers[] = $edit['customer'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer don't exists / create customer
|
||||||
|
else {
|
||||||
|
$new = $this->create_customer( array( 'customer' => $_customer ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $new ) ) {
|
||||||
|
$customers[] = array(
|
||||||
|
'id' => $customer_id,
|
||||||
|
'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() )
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$customers[] = $new['customer'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'customers' => apply_filters( 'woocommerce_api_customers_bulk_response', $customers, $this ) );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce API Exception Class
|
||||||
|
*
|
||||||
|
* Extends Exception to provide additional data
|
||||||
|
*
|
||||||
|
* @author WooThemes
|
||||||
|
* @category API
|
||||||
|
* @package WooCommerce/API
|
||||||
|
* @since 2.2
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly
|
||||||
|
}
|
||||||
|
|
||||||
|
class WC_API_Exception extends Exception {
|
||||||
|
|
||||||
|
/** @var string sanitized error code */
|
||||||
|
protected $error_code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup exception, requires 3 params:
|
||||||
|
*
|
||||||
|
* error code - machine-readable, e.g. `woocommerce_invalid_product_id`
|
||||||
|
* error message - friendly message, e.g. 'Product ID is invalid'
|
||||||
|
* http status code - proper HTTP status code to respond with, e.g. 400
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param string $error_code
|
||||||
|
* @param string $error_message user-friendly translated error message
|
||||||
|
* @param int $http_status_code HTTP status code to respond with
|
||||||
|
*/
|
||||||
|
public function __construct( $error_code, $error_message, $http_status_code ) {
|
||||||
|
$this->error_code = $error_code;
|
||||||
|
parent::__construct( $error_message, $http_status_code );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the error code
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getErrorCode() {
|
||||||
|
return $this->error_code;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?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 sprintf( '%s; charset=%s', isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json', 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_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// see http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
|
||||||
|
WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' );
|
||||||
|
|
||||||
|
// Prepend '/**/' to mitigate possible JSONP Flash attacks
|
||||||
|
return '/**/' . $_GET['_jsonp'] . '(' . json_encode( $data ) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode( $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,328 @@
|
||||||
|
<?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 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /reports/sales/top_sellers
|
||||||
|
$routes[ $this->base . '/sales/top_sellers' ] = array(
|
||||||
|
array( array( $this, 'get_top_sellers_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', 'sales/top_sellers' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
// check for WP_Error
|
||||||
|
if ( is_wp_error( $check ) ) {
|
||||||
|
return $check;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set date filtering
|
||||||
|
$this->setup_report( $filter );
|
||||||
|
|
||||||
|
// new customers
|
||||||
|
$users_query = new WP_User_Query(
|
||||||
|
array(
|
||||||
|
'fields' => array( 'user_registered' ),
|
||||||
|
'role' => 'customer',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$customers = $users_query->get_results();
|
||||||
|
|
||||||
|
foreach ( $customers as $key => $customer ) {
|
||||||
|
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
|
||||||
|
unset( $customers[ $key ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_customers = count( $customers );
|
||||||
|
$report_data = $this->report->get_report_data();
|
||||||
|
$period_totals = array();
|
||||||
|
|
||||||
|
// setup period totals by ensuring each period in the interval has data
|
||||||
|
for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
|
||||||
|
|
||||||
|
switch ( $this->report->chart_groupby ) {
|
||||||
|
case 'day' :
|
||||||
|
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the customer signups for each period
|
||||||
|
$customer_count = 0;
|
||||||
|
foreach ( $customers as $customer ) {
|
||||||
|
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
|
||||||
|
$customer_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_totals[ $time ] = array(
|
||||||
|
'sales' => wc_format_decimal( 0.00, 2 ),
|
||||||
|
'orders' => 0,
|
||||||
|
'items' => 0,
|
||||||
|
'tax' => wc_format_decimal( 0.00, 2 ),
|
||||||
|
'shipping' => wc_format_decimal( 0.00, 2 ),
|
||||||
|
'discount' => wc_format_decimal( 0.00, 2 ),
|
||||||
|
'customers' => $customer_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add total sales, total order count, total tax and total shipping for each period
|
||||||
|
foreach ( $report_data->orders as $order ) {
|
||||||
|
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||||
|
|
||||||
|
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
|
||||||
|
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
|
||||||
|
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $report_data->order_counts as $order ) {
|
||||||
|
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
|
||||||
|
|
||||||
|
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_totals[ $time ]['orders'] = (int) $order->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add total order items for each period
|
||||||
|
foreach ( $report_data->order_items as $order_item ) {
|
||||||
|
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
|
||||||
|
|
||||||
|
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add total discount for each period
|
||||||
|
foreach ( $report_data->coupons as $discount ) {
|
||||||
|
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
|
||||||
|
|
||||||
|
if ( ! isset( $period_totals[ $time ] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$sales_data = array(
|
||||||
|
'total_sales' => $report_data->total_sales,
|
||||||
|
'net_sales' => $report_data->net_sales,
|
||||||
|
'average_sales' => $report_data->average_sales,
|
||||||
|
'total_orders' => $report_data->total_orders,
|
||||||
|
'total_items' => $report_data->total_items,
|
||||||
|
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
|
||||||
|
'total_shipping' => $report_data->total_shipping,
|
||||||
|
'total_refunds' => $report_data->total_refunds,
|
||||||
|
'total_discount' => $report_data->total_coupons,
|
||||||
|
'totals_grouped_by' => $this->report->chart_groupby,
|
||||||
|
'totals' => $period_totals,
|
||||||
|
'total_customers' => $total_customers,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the top sellers report
|
||||||
|
*
|
||||||
|
* @since 2.1
|
||||||
|
* @param string $fields fields to include in response
|
||||||
|
* @param array $filter date filtering
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_top_sellers_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 );
|
||||||
|
|
||||||
|
$top_sellers = $this->report->get_order_report_data( array(
|
||||||
|
'data' => array(
|
||||||
|
'_product_id' => array(
|
||||||
|
'type' => 'order_item_meta',
|
||||||
|
'order_item_type' => 'line_item',
|
||||||
|
'function' => '',
|
||||||
|
'name' => 'product_id'
|
||||||
|
),
|
||||||
|
'_qty' => array(
|
||||||
|
'type' => 'order_item_meta',
|
||||||
|
'order_item_type' => 'line_item',
|
||||||
|
'function' => 'SUM',
|
||||||
|
'name' => 'order_item_qty'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'order_by' => 'order_item_qty DESC',
|
||||||
|
'group_by' => 'product_id',
|
||||||
|
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
|
||||||
|
'query_type' => 'get_results',
|
||||||
|
'filter_range' => true,
|
||||||
|
) );
|
||||||
|
|
||||||
|
$top_sellers_data = array();
|
||||||
|
|
||||||
|
foreach ( $top_sellers as $top_seller ) {
|
||||||
|
|
||||||
|
$product = wc_get_product( $top_seller->product_id );
|
||||||
|
|
||||||
|
if ( $product ) {
|
||||||
|
$top_sellers_data[] = array(
|
||||||
|
'title' => $product->get_title(),
|
||||||
|
'product_id' => $top_seller->product_id,
|
||||||
|
'quantity' => $top_seller->order_item_qty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $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' );
|
||||||
|
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
|
||||||
|
|
||||||
|
$this->report = new WC_Report_Sales_By_Date();
|
||||||
|
|
||||||
|
if ( empty( $filter['period'] ) ) {
|
||||||
|
|
||||||
|
// custom date range
|
||||||
|
$filter['period'] = 'custom';
|
||||||
|
|
||||||
|
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
|
||||||
|
|
||||||
|
// 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,458 @@
|
||||||
|
<?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' ) );
|
||||||
|
|
||||||
|
// maybe add meta to top-level resource responses
|
||||||
|
foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
|
||||||
|
add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_names = array( 'order', 'coupon', 'customer', 'product', 'report',
|
||||||
|
'customer_orders', 'customer_downloads', 'order_note', 'order_refund',
|
||||||
|
'product_reviews', 'product_category'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $response_names as $name ) {
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
*/
|
||||||
|
add_filter( "woocommerce_api_{$name}_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 || 'shop_webhook' === $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 );
|
||||||
|
|
||||||
|
if ( null === $post ) {
|
||||||
|
return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %s found with the ID equal to %s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// order (ASC or DESC, ASC by default)
|
||||||
|
if ( ! empty( $request_args['order'] ) ) {
|
||||||
|
$args['order'] = $request_args['order'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderby
|
||||||
|
if ( ! empty( $request_args['orderby'] ) ) {
|
||||||
|
$args['orderby'] = $request_args['orderby'];
|
||||||
|
|
||||||
|
// allow sorting by meta value
|
||||||
|
if ( ! empty( $request_args['orderby_meta_key'] ) ) {
|
||||||
|
$args['meta_key'] = $request_args['orderby_meta_key'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow post status change
|
||||||
|
if ( ! empty( $request_args['post_status'] ) ) {
|
||||||
|
$args['post_status'] = $request_args['post_status'];
|
||||||
|
unset( $request_args['post_status'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by a list of post id
|
||||||
|
if ( ! empty( $request_args['in'] ) ) {
|
||||||
|
$args['post__in'] = explode( ',', $request_args['in'] );
|
||||||
|
unset( $request_args['in'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// resource page
|
||||||
|
$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
|
||||||
|
|
||||||
|
$args = apply_filters( 'woocommerce_api_query_args', $args, $request_args );
|
||||||
|
|
||||||
|
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 ( ! is_array( $data ) || 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/webhook
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_null( $post ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_type = get_post_type_object( $post->post_type );
|
||||||
|
|
||||||
|
if ( 'read' === $context ) {
|
||||||
|
return ( 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $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,750 @@
|
||||||
|
<?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'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// load response handler
|
||||||
|
$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 array( 'errors' => $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( 'errors' => 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', 'woocommerce' ), 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', 'woocommerce' ), 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', 'woocommerce' ), array( 'status' => 404 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* urldecode deep.
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param string/array $value Data to decode with urldecode.
|
||||||
|
* @return string/array Decoded data.
|
||||||
|
*/
|
||||||
|
protected function urldecode_deep( $value ) {
|
||||||
|
if ( is_array( $value ) ) {
|
||||||
|
return array_map( array( $this, 'urldecode_deep' ), $value );
|
||||||
|
} else {
|
||||||
|
return urldecode( $value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.2
|
||||||
|
* @param callable|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
|
||||||
|
if ( 'data' == $param->getName() ) {
|
||||||
|
$ordered_parameters[] = $provided[ $param->getName() ];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordered_parameters[] = $this->urldecode_deep( $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', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ordered_parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the site index.
|
||||||
|
*
|
||||||
|
* This endpoint describes the capabilities of the site.
|
||||||
|
*
|
||||||
|
* @since 2.3
|
||||||
|
* @return array Index entity
|
||||||
|
*/
|
||||||
|
public function get_index() {
|
||||||
|
|
||||||
|
// General site data
|
||||||
|
$available = array( 'store' => array(
|
||||||
|
'name' => get_option( 'blogname' ),
|
||||||
|
'description' => get_option( 'blogdescription' ),
|
||||||
|
'URL' => get_option( 'siteurl' ),
|
||||||
|
'wc_version' => WC()->version,
|
||||||
|
'routes' => array(),
|
||||||
|
'meta' => array(
|
||||||
|
'timezone' => wc_timezone_string(),
|
||||||
|
'currency' => get_woocommerce_currency(),
|
||||||
|
'currency_format' => get_woocommerce_currency_symbol(),
|
||||||
|
'currency_position' => get_option( 'woocommerce_currency_pos' ),
|
||||||
|
'thousand_separator' => get_option( 'woocommerce_price_decimal_sep' ),
|
||||||
|
'decimal_separator' => get_option( 'woocommerce_price_thousand_sep' ),
|
||||||
|
'price_num_decimals' => wc_get_price_decimals(),
|
||||||
|
'tax_included' => wc_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' ) ),
|
||||||
|
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
|
||||||
|
'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
|
||||||
|
'links' => array(
|
||||||
|
'help' => 'http://woothemes.github.io/woocommerce-rest-api-docs/',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
|
||||||
|
// Find the available routes
|
||||||
|
foreach ( $this->get_routes() as $route => $callbacks ) {
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
|
||||||
|
|
||||||
|
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['store']['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 associative array
|
||||||
|
*/
|
||||||
|
public function link_header( $rel, $link, $other = array() ) {
|
||||||
|
|
||||||
|
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $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' ) ) {
|
||||||
|
|
||||||
|
$single = count( $query->get_results() ) == 1;
|
||||||
|
$total = $query->get_total();
|
||||||
|
|
||||||
|
if( $query->get( 'number' ) > 0 ) {
|
||||||
|
$page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
|
||||||
|
$total_pages = ceil( $total / $query->get( 'number' ) );
|
||||||
|
} else {
|
||||||
|
$page = 1;
|
||||||
|
$total_pages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WP_Query
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$page = $query->get( 'paged' );
|
||||||
|
$single = $query->is_single();
|
||||||
|
$total = $query->found_posts;
|
||||||
|
$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 parameter 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 ) );
|
||||||
|
|
||||||
|
// get the home host
|
||||||
|
$host = parse_url( get_home_url(), PHP_URL_HOST );
|
||||||
|
|
||||||
|
return set_url_scheme( "http://{$host}{$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( wc_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,462 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce API Webhooks class
|
||||||
|
*
|
||||||
|
* Handles requests to the /webhooks endpoint
|
||||||
|
*
|
||||||
|
* @author WooThemes
|
||||||
|
* @category API
|
||||||
|
* @package WooCommerce/API
|
||||||
|
* @since 2.2
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly
|
||||||
|
}
|
||||||
|
|
||||||
|
class WC_API_Webhooks extends WC_API_Resource {
|
||||||
|
|
||||||
|
/** @var string $base the route base */
|
||||||
|
protected $base = '/webhooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the routes for this class
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $routes
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function register_routes( $routes ) {
|
||||||
|
|
||||||
|
# GET|POST /webhooks
|
||||||
|
$routes[ $this->base ] = array(
|
||||||
|
array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
|
||||||
|
array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /webhooks/count
|
||||||
|
$routes[ $this->base . '/count'] = array(
|
||||||
|
array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET|PUT|DELETE /webhooks/<id>
|
||||||
|
$routes[ $this->base . '/(?P<id>\d+)' ] = array(
|
||||||
|
array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
|
||||||
|
array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
|
||||||
|
array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /webhooks/<id>/deliveries
|
||||||
|
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
|
||||||
|
array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
# GET /webhooks/<webhook_id>/deliveries/<id>
|
||||||
|
$routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
|
||||||
|
array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all webhooks
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $fields
|
||||||
|
* @param array $filter
|
||||||
|
* @param int $page
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
|
||||||
|
|
||||||
|
if ( ! empty( $status ) ) {
|
||||||
|
$filter['status'] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter['page'] = $page;
|
||||||
|
|
||||||
|
$query = $this->query_webhooks( $filter );
|
||||||
|
|
||||||
|
$webhooks = array();
|
||||||
|
|
||||||
|
foreach ( $query->posts as $webhook_id ) {
|
||||||
|
|
||||||
|
if ( ! $this->is_readable( $webhook_id ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->server->add_pagination_headers( $query );
|
||||||
|
|
||||||
|
return array( 'webhooks' => $webhooks );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the webhook for the given ID
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id webhook ID
|
||||||
|
* @param array $fields
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_webhook( $id, $fields = null ) {
|
||||||
|
|
||||||
|
// ensure webhook ID is valid & user has permission to read
|
||||||
|
$id = $this->validate_request( $id, 'shop_webhook', 'read' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhook = new WC_Webhook( $id );
|
||||||
|
|
||||||
|
$webhook_data = array(
|
||||||
|
'id' => $webhook->id,
|
||||||
|
'name' => $webhook->get_name(),
|
||||||
|
'status' => $webhook->get_status(),
|
||||||
|
'topic' => $webhook->get_topic(),
|
||||||
|
'resource' => $webhook->get_resource(),
|
||||||
|
'event' => $webhook->get_event(),
|
||||||
|
'hooks' => $webhook->get_hooks(),
|
||||||
|
'delivery_url' => $webhook->get_delivery_url(),
|
||||||
|
'created_at' => $this->server->format_datetime( $webhook->get_post_data()->post_date_gmt ),
|
||||||
|
'updated_at' => $this->server->format_datetime( $webhook->get_post_data()->post_modified_gmt ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of webhooks
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param string $status
|
||||||
|
* @param array $filter
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_webhooks_count( $status = null, $filter = array() ) {
|
||||||
|
try {
|
||||||
|
if ( ! current_user_can( 'read_private_shop_webhooks' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $status ) ) {
|
||||||
|
$filter['status'] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->query_webhooks( $filter );
|
||||||
|
|
||||||
|
return array( 'count' => (int) $query->found_posts );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an webhook
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $data parsed webhook data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create_webhook( $data ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['webhook'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['webhook'];
|
||||||
|
|
||||||
|
// permission check
|
||||||
|
if ( ! current_user_can( 'publish_shop_webhooks' ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks', 'woocommerce' ), 401 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
|
||||||
|
|
||||||
|
// validate topic
|
||||||
|
if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate delivery URL
|
||||||
|
if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
|
||||||
|
'post_type' => 'shop_webhook',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'ping_status' => 'closed',
|
||||||
|
'post_author' => get_current_user_id(),
|
||||||
|
'post_password' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password,
|
||||||
|
'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
|
||||||
|
), $data, $this );
|
||||||
|
|
||||||
|
$webhook_id = wp_insert_post( $webhook_data );
|
||||||
|
|
||||||
|
if ( is_wp_error( $webhook_id ) || ! $webhook_id ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_cannot_create_webhook', sprintf( __( 'Cannot create webhook: %s', 'woocommerce' ), is_wp_error( $webhook_id ) ? implode( ', ', $webhook_id->get_error_messages() ) : '0' ), 500 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhook = new WC_Webhook( $webhook_id );
|
||||||
|
|
||||||
|
// set topic, delivery URL, and optional secret
|
||||||
|
$webhook->set_topic( $data['topic'] );
|
||||||
|
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||||
|
|
||||||
|
// set secret if provided, defaults to API users consumer secret
|
||||||
|
$webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : get_user_meta( get_current_user_id(), 'woocommerce_api_consumer_secret', true ) );
|
||||||
|
|
||||||
|
// send ping
|
||||||
|
$webhook->deliver_ping();
|
||||||
|
|
||||||
|
// HTTP 201 Created
|
||||||
|
$this->server->send_status( 201 );
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_create_webhook', $webhook->id, $this );
|
||||||
|
|
||||||
|
delete_transient( 'woocommerce_webhook_ids' );
|
||||||
|
|
||||||
|
return $this->get_webhook( $webhook->id );
|
||||||
|
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a webhook
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id webhook ID
|
||||||
|
* @param array $data parsed webhook data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function edit_webhook( $id, $data ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( ! isset( $data['webhook'] ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $data['webhook'];
|
||||||
|
|
||||||
|
$id = $this->validate_request( $id, 'shop_webhook', 'edit' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
|
||||||
|
|
||||||
|
$webhook = new WC_Webhook( $id );
|
||||||
|
|
||||||
|
// update topic
|
||||||
|
if ( ! empty( $data['topic'] ) ) {
|
||||||
|
|
||||||
|
if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
|
||||||
|
|
||||||
|
$webhook->set_topic( $data['topic'] );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update delivery URL
|
||||||
|
if ( ! empty( $data['delivery_url'] ) ) {
|
||||||
|
if ( wc_is_valid_url( $data['delivery_url'] ) ) {
|
||||||
|
|
||||||
|
$webhook->set_delivery_url( $data['delivery_url'] );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update secret
|
||||||
|
if ( ! empty( $data['secret'] ) ) {
|
||||||
|
$webhook->set_secret( $data['secret'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// update status
|
||||||
|
if ( ! empty( $data['status'] ) ) {
|
||||||
|
$webhook->update_status( $data['status'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user ID
|
||||||
|
$webhook_data = array(
|
||||||
|
'ID' => $webhook->id,
|
||||||
|
'post_author' => get_current_user_id()
|
||||||
|
);
|
||||||
|
|
||||||
|
// update name
|
||||||
|
if ( ! empty( $data['name'] ) ) {
|
||||||
|
$webhook_data['post_title'] = $data['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// update post
|
||||||
|
wp_update_post( $webhook_data );
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_edit_webhook', $webhook->id, $this );
|
||||||
|
|
||||||
|
delete_transient( 'woocommerce_webhook_ids' );
|
||||||
|
|
||||||
|
return $this->get_webhook( $id );
|
||||||
|
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a webhook
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param int $id webhook ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function delete_webhook( $id ) {
|
||||||
|
|
||||||
|
$id = $this->validate_request( $id, 'shop_webhook', 'delete' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $id ) ) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action( 'woocommerce_api_delete_webhook', $id, $this );
|
||||||
|
|
||||||
|
delete_transient( 'woocommerce_webhook_ids' );
|
||||||
|
|
||||||
|
// no way to manage trashed webhooks at the moment, so force delete
|
||||||
|
return $this->delete( $id, 'webhook', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get webhook post objects
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param array $args request arguments for filtering query
|
||||||
|
* @return WP_Query
|
||||||
|
*/
|
||||||
|
private function query_webhooks( $args ) {
|
||||||
|
|
||||||
|
// Set base query arguments
|
||||||
|
$query_args = array(
|
||||||
|
'fields' => 'ids',
|
||||||
|
'post_type' => 'shop_webhook',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add status argument
|
||||||
|
if ( ! empty( $args['status'] ) ) {
|
||||||
|
|
||||||
|
switch ( $args['status'] ) {
|
||||||
|
case 'active':
|
||||||
|
$query_args['post_status'] = 'publish';
|
||||||
|
break;
|
||||||
|
case 'paused':
|
||||||
|
$query_args['post_status'] = 'draft';
|
||||||
|
break;
|
||||||
|
case 'disabled':
|
||||||
|
$query_args['post_status'] = 'pending';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$query_args['post_status'] = 'publish';
|
||||||
|
}
|
||||||
|
unset( $args['status'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$query_args = $this->merge_query_args( $query_args, $args );
|
||||||
|
|
||||||
|
return new WP_Query( $query_args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deliveries for a webhook
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param string $webhook_id webhook ID
|
||||||
|
* @param string|null $fields fields to include in response
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_webhook_deliveries( $webhook_id, $fields = null ) {
|
||||||
|
|
||||||
|
// Ensure ID is valid webhook ID
|
||||||
|
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $webhook_id ) ) {
|
||||||
|
return $webhook_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhook = new WC_Webhook( $webhook_id );
|
||||||
|
$logs = $webhook->get_delivery_logs();
|
||||||
|
$delivery_logs = array();
|
||||||
|
|
||||||
|
foreach ( $logs as $log ) {
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
$log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt );
|
||||||
|
|
||||||
|
// Remove comment object
|
||||||
|
unset( $log['comment'] );
|
||||||
|
|
||||||
|
$delivery_logs[] = $log;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array( 'webhook_deliveries' => $delivery_logs );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the delivery log for the given webhook ID and delivery ID
|
||||||
|
*
|
||||||
|
* @since 2.2
|
||||||
|
* @param string $webhook_id webhook ID
|
||||||
|
* @param string $id delivery log ID
|
||||||
|
* @param string|null $fields fields to limit response to
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
|
||||||
|
try {
|
||||||
|
// Validate webhook ID
|
||||||
|
$webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $webhook_id ) ) {
|
||||||
|
return $webhook_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = absint( $id );
|
||||||
|
|
||||||
|
if ( empty( $id ) ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID', 'woocommerce' ), 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhook = new WC_Webhook( $webhook_id );
|
||||||
|
|
||||||
|
$log = $webhook->get_delivery_log( $id );
|
||||||
|
|
||||||
|
if ( ! $log ) {
|
||||||
|
throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery', 'woocommerce' ), 400 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$delivery_log = $log;
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
$delivery_log['created_at'] = $this->server->format_datetime( $log['comment']->comment_date_gmt );
|
||||||
|
|
||||||
|
// Remove comment object
|
||||||
|
unset( $delivery_log['comment'] );
|
||||||
|
|
||||||
|
return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', $delivery_log, $id, $fields, $log, $webhook_id, $this ) );
|
||||||
|
} catch ( WC_API_Exception $e ) {
|
||||||
|
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?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 );
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue