<?php
use Automattic\Jetpack\Constants;
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_API_Client' ) ) {
abstract class WC_Connect_API_Client {
const API_VERSION = WOOCOMMERCE_CONNECT_SERVER_API_VERSION;
/**
* @var WC_Connect_Services_Validator
*/
protected $validator;
/**
* @var WC_Connect_Loader
*/
protected $wc_connect_loader;
public function __construct(
WC_Connect_Service_Schemas_Validator $validator,
WC_Connect_Loader $wc_connect_loader
) {
$this->validator = $validator;
$this->wc_connect_loader = $wc_connect_loader;
}
/**
* Requests the available services for this site from the WooCommerce Shipping & Tax Server
*
* @return array|WP_Error
*/
public function get_service_schemas() {
$response_body = $this->request( 'POST', '/services' );
if ( is_wp_error( $response_body ) ) {
return $response_body;
}
$result = $this->validator->validate_service_schemas( $response_body );
if ( is_wp_error( $result ) ) {
return $result;
}
return $response_body;
}
/**
* Validates the settings for a given service with the WooCommerce Shipping & Tax Server
*
* @param $service_slug
* @param $service_settings
*
* @return bool|WP_Error
*/
public function validate_service_settings( $service_slug, $service_settings ) {
// Make sure the service slug only contains dashes, underscores or letters
if ( 1 === preg_match( '/[^a-z_\-]/i', $service_slug ) ) {
return new WP_Error( 'invalid_service_slug', __( 'Invalid WooCommerce Shipping & Tax service slug provided', 'woocommerce-services' ) );
}
return $this->request( 'POST', "/services/{$service_slug}/settings", array( 'service_settings' => $service_settings ) );
}
/**
* Build the server's expected contents array, for rates requests.
*
* @param $package Package provided to WC_Shipping_Method::calculate_shipping()
*
* @return array|WP_Error {
* @type float $height Product height.
* @type float $width Product width.
* @type float $length Product length.
* @type int $product_id Product ID (or Variation ID).
* @type int $quantity Quantity of product in shipment.
* @type float $weight Product weight.
* }
*/
public function build_shipment_contents( $package ) {
$contents = array();
foreach ( $package['contents'] as $package_item ) {
$product = $package_item['data'];
$quantity = $package_item['quantity'];
if ( ( $quantity > 0 ) && $product->needs_shipping() ) {
if ( ! $product->has_weight() ) {
return new WP_Error(
'product_missing_weight',
sprintf(
__( 'Product ( ID: %d ) did not include a weight. Shipping rates cannot be calculated.', 'woocommerce-services' ),
$product->get_id()
),
array( 'product_id' => $product->get_id() )
);
}
if (
! $product->get_length() ||
! $product->get_height() ||
! $product->get_width()
) {
return new WP_Error(
'product_missing_dimension',
sprintf(
__( 'Product ( ID: %d ) is missing a dimension value. Shipping rates cannot be calculated.', 'woocommerce-services' ),
$product->get_id()
),
array( 'product_id' => $product->get_id() )
);
}
$weight = $product->get_weight();
$height = $product->get_height();
$length = $product->get_length();
$width = $product->get_width();
$contents[] = array(
'height' => (float) $height,
'product_id' => $product->get_id(),
'length' => (float) $length,
'quantity' => $package_item['quantity'],
'weight' => (float) $weight,
'width' => (float) $width,
);
}
}
return $contents;
}
/**
* Gets shipping rates (for checkout) from the WooCommerce Shipping & Tax Server
*
* @param $services All settings for all services we want rates for
* @param $package Package provided to WC_Shipping_Method::calculate_shipping()
* @param $custom_boxes array of custom boxes definitions (objects)
* @param $predefined_boxes array of enabled predefined box IDs (strings)
*
* @return object|WP_Error
*/
public function get_shipping_rates( $services, $package, $custom_boxes, $predefined_boxes ) {
// First, build the contents array
// each item needs to specify quantity, weight, length, width and height
$contents = $this->build_shipment_contents( $package );
if ( is_wp_error( $contents ) ) {
return $contents;
}
if ( empty( $contents ) ) {
return new WP_Error(
'nothing_to_ship',
__( 'No shipping rate could be calculated. No items in the package are shippable.', 'woocommerce-services' )
);
}
// Then, make the request
$body = array(
'contents' => $contents,
'destination' => $package['destination'],
'services' => $services,
'boxes' => $custom_boxes,
'predefined_boxes' => $predefined_boxes,
);
return $this->request( 'POST', '/shipping/rates', $body );
}
/**
* Send rates request information to track subscription events
*
* @param array $services Array of service settings for shipping methods.
*
* @return object|WP_Error
*/
public function track_subscription_event( $services ) {
return $this->request( 'POST', '/subscriptions/checkout', array( 'services' => $services ) );
}
public function send_shipping_label_request( $body ) {
return $this->request( 'POST', '/shipping/label', $body );
}
public function send_address_normalization_request( $body ) {
return $this->request( 'POST', '/shipping/address/normalize', $body );
}
/**
* Asks the WooCommerce Shipping & Tax server for an array of payment methods
*
* @return mixed|WP_Error
*/
public function get_payment_methods() {
return $this->request( 'POST', '/payment/methods' );
}
/**
* Retrieve Sift configurations.
*
* @return object|WP_Error
*/
public function get_sift_configuration() {
return $this->request( 'GET', '/payment/sift' );
}
/**
* Gets shipping rates (for labels) from the WooCommerce Shipping & Tax Server
*
* @param array $request - array(
* 'packages' => array(
* array(
* 'id' => 'box_1',
* 'height' => 10,
* 'length' => 10,
* 'width' => 10,
* 'weight' => 10,
* ),
* array(
* 'id' => 'box_2',
* 'box_id' => 'medium_flat_box_top',
* 'weight' => 5,
* ),
* ...
* ),
* 'carrier' => 'usps',
* 'origin' => array(
* 'address' => '132 Hawthorne St',
* 'address_2' => '',
* 'city' => 'San Francisco',
* 'state' => 'CA',
* 'postcode' => '94107',
* 'country' => 'US',
* ),
* 'destination' => array(
* 'address' => '1550 Snow Creek Dr',
* 'address_2' => '',
* 'city' => 'Park City',
* 'state' => 'UT',
* 'postcode' => '84060',
* 'country' => 'US',
* ),
* )
* @return object|WP_Error
*/
public function get_label_rates( $request ) {
return $this->request( 'POST', '/shipping/label/rates', $request );
}
/**
* Gets a PDF with the set of dummy labels specified in the request
*
* @param $request
* @return object|WP_Error
*/
public function get_labels_preview_pdf( $request ) {
return $this->request( 'POST', 'shipping/labels/preview', $request );
}
/**
* Gets a PDF with the requested shipping labels in it
*
* @param $request
* @return object|WP_Error
*/
public function get_labels_print_pdf( $request ) {
return $this->request( 'POST', 'shipping/labels/print', $request );
}
/**
* Gets the shipping label status (refund status, tracking code, etc)
*
* @param $label_id integer
* @return object|WP_Error
*/
public function get_label_status( $label_id ) {
return $this->request( 'GET', '/shipping/label/' . $label_id . '?get_refund=true' );
}
/**
* Gets the shipping label status (refund status, tracking code, etc)
*
* @param $order_id integer
* @return object|WP_Error
*/
public function anonymize_order( $order_id ) {
return $this->request( 'POST', '/privacy/order/' . $order_id . '/anonymize' );
}
/**
* Request a refund for a given shipping label
*
* @param $label_id integer
* @return object|WP_Error
*/
public function send_shipping_label_refund_request( $label_id ) {
return $this->request( 'POST', '/shipping/label/' . $label_id . '/refund' );
}
/**
* Gets the configured carrier accounts
*
* @param $request
* @return object|WP_Error
*/
public function get_carrier_accounts() {
return $this->request( 'GET', '/shipping/carriers' );
}
/**
* Disconnects the provided carrier account
*
* @param $carrier_id
* @return object|WP_Error
*/
public function disconnect_carrier_account( $carrier_id ) {
return $this->request( 'DELETE', '/shipping/carrier/' . $carrier_id );
}
/**
* Register a new carrier account
*
* @param $body
* @return object|WP_Error
*/
public function create_shipping_carrier_account( $body ) {
return $this->request( 'POST', '/shipping/carrier', $body );
}
/**
* Get a list of the subscriptions for WooCommerce.com linked account.
*
* @param $body
* @param object|WP_Error
*/
public function get_wccom_subscriptions() {
return $this->request( 'POST', '/subscriptions' );
}
/**
* Get all carriers we support for registration. This end point
* returns a list of "fields" that we use to register the carrier
* account.
*
* @return object|WP_Error
*/
public function get_carrier_types() {
return $this->request( 'GET', '/shipping/carrier-types' );
}
/**
* Tests the connection to the WooCommerce Shipping & Tax Server
*
* @return true|WP_Error
*/
public function auth_test() {
return $this->request( 'GET', '/connection/test' );
}
/** Heartbeat test.
*
* @return true|WP_Error
*/
public function is_alive() {
return $this->request( 'GET', '' );
}
/** Heartbeat test with a transient cache.
*
* @return true|WP_Error
*/
public function is_alive_cached() {
$connect_server_is_alive_transient = get_transient( 'connect_server_is_alive_transient' );
if ( false !== $connect_server_is_alive_transient ) {
return true;
}
$is_alive_request = $this->is_alive();
$new_is_alive = ! is_wp_error( $is_alive_request );
if ( $new_is_alive ) {
set_transient( 'connect_server_is_alive_transient', true, MINUTE_IN_SECONDS );
}
return $new_is_alive;
}
/**
* Activate a subscrption with WCCOM API.
*
* @param string $subscription_key Product Key on WCCOM.
* @return WP_Error|Array API Response.
*/
public function activate_subscription( $subscription_key ) {
$activation_response = WC_Helper_API::post(
'activate',
array(
'authenticated' => true,
'body' => wp_json_encode(
array(
'product_key' => $subscription_key,
)
),
)
);
return $activation_response;
}
/**
* Sends a request to the WooCommerce Shipping & Tax Server
*
* @param $method
* @param $path
* @param $body
* @return mixed|WP_Error
*/
abstract protected function request( $method, $path, $body = array() );
/**
* Proxy an HTTP request through the WCS Server
*
* @param $path Path of proxy route
* @param $args WP_Http request args
*
* @return array|WP_Error
*/
public function proxy_request( $path, $args ) {
$proxy_url = trailingslashit( WOOCOMMERCE_CONNECT_SERVER_URL );
$proxy_url .= ltrim( $path, '/' );
$authorization = $this->authorization_header();
if ( is_wp_error( $authorization ) ) {
return $authorization;
}
$args['headers']['Authorization'] = $authorization;
$http_timeout = 60; // 1 minute
if ( function_exists( 'wc_set_time_limit' ) ) {
wc_set_time_limit( $http_timeout + 10 );
}
$args['timeout'] = $http_timeout;
$response = wp_remote_request( $proxy_url, $args );
return $response;
}
/**
* Adds useful WP/WC/WCC information to request bodies
*
* @param array $initial_body
* @return array
*/
protected function request_body( $initial_body = array() ) {
$default_body = array(
'settings' => array(),
);
$body = array_merge( $default_body, $initial_body );
// Add interesting fields to the body of each request
$body['settings'] = wp_parse_args(
$body['settings'],
array(
'store_guid' => $this->get_guid(),
'base_city' => WC()->countries->get_base_city(),
'base_country' => WC()->countries->get_base_country(),
'base_state' => WC()->countries->get_base_state(),
'base_postcode' => WC()->countries->get_base_postcode(),
'currency' => get_woocommerce_currency(),
'dimension_unit' => strtolower( get_option( 'woocommerce_dimension_unit' ) ),
'weight_unit' => strtolower( get_option( 'woocommerce_weight_unit' ) ),
'wcs_version' => WC_Connect_Loader::get_wcs_version(),
'jetpack_version' => 'embed-' . WC_Connect_Jetpack::get_jetpack_connection_package_version(),
'is_atomic' => WC_Connect_Jetpack::is_atomic_site(),
'wc_version' => WC()->version,
'wp_version' => get_bloginfo( 'version' ),
'last_services_update' => WC_Connect_Options::get_option( 'services_last_update', 0 ),
'last_heartbeat' => WC_Connect_Options::get_option( 'last_heartbeat', 0 ),
'last_rate_request' => WC_Connect_Options::get_option( 'last_rate_request', 0 ),
'active_services' => $this->wc_connect_loader->get_active_services(),
'disable_stats' => WC_Connect_Jetpack::is_staging_site(),
'taxes_enabled' => wc_tax_enabled() && 'yes' === get_option( 'wc_connect_taxes_enabled' ),
)
);
return $body;
}
/**
* Generates headers for our request to the WooCommerce Shipping & Tax Server
*
* @return array|WP_Error
*/
protected function request_headers() {
$authorization = $this->authorization_header();
if ( is_wp_error( $authorization ) ) {
return $authorization;
}
$headers = array();
$locale = strtolower( str_replace( '_', '-', get_locale() ) );
$locale_elements = explode( '-', $locale );
$lang = $locale_elements[0];
$headers['Accept-Language'] = $locale . ',' . $lang;
$headers['Content-Type'] = 'application/json; charset=utf-8';
$headers['Accept'] = 'application/vnd.woocommerce-connect.v' . static::API_VERSION;
$headers['Authorization'] = $authorization;
$wc_helper_auth_info = WC_Connect_Functions::get_wc_helper_auth_info();
if ( ! is_wp_error( $wc_helper_auth_info ) ) {
$headers['X-Woo-Signature'] = $this->request_signature_wccom( $wc_helper_auth_info['access_token_secret'], 'subscriptions', 'GET', array() );
$headers['X-Woo-Access-Token'] = $wc_helper_auth_info['access_token'];
$headers['X-Woo-Site-Id'] = $wc_helper_auth_info['site_id'];
}
return $headers;
}
protected function authorization_header() {
$token = WC_Connect_Jetpack::get_blog_access_token();
$token = apply_filters( 'wc_connect_jetpack_access_token', $token );
if ( ! $token || empty( $token->secret ) ) {
return new WP_Error(
'missing_token',
__( 'Unable to send request to WooCommerce Shipping & Tax server. WordPress.com token is missing', 'woocommerce-services' )
);
}
if ( false === strpos( $token->secret, '.' ) ) {
return new WP_Error(
'invalid_token',
__( 'Unable to send request to WooCommerce Shipping & Tax server. WordPress.com token is malformed.', 'woocommerce-services' )
);
}
list( $token_key, $token_secret ) = explode( '.', $token->secret );
$token_key = sprintf( '%s:%d:%d', $token_key, Constants::get_constant( 'JETPACK__API_VERSION' ), $token->external_user_id );
$time_diff = (int) Jetpack_Options::get_option( 'time_diff' );
$timestamp = time() + $time_diff;
$nonce = wp_generate_password( 10, false );
$signature = $this->request_signature( $token_key, $token_secret, $timestamp, $nonce, $time_diff );
if ( is_wp_error( $signature ) ) {
return $signature;
}
$auth = array(
'token' => $token_key,
'timestamp' => $timestamp,
'nonce' => $nonce,
'signature' => $signature,
);
$header_pieces = array();
foreach ( $auth as $key => $value ) {
$header_pieces[] = sprintf( '%s="%s"', $key, $value );
}
$authorization = 'X_JP_Auth ' . join( ' ', $header_pieces );
return $authorization;
}
/**
* Generate a signature for WCCOM API request validation.
*
* @param string $token_secret
* @param string $endpoint
* @param string $method
* @param array $body
* @return string
*/
protected function request_signature_wccom( $token_secret, $endpoint, $method, $body = array() ) {
$request_url = WC_Helper_API::url( $endpoint );
$data = array(
'host' => parse_url( $request_url, PHP_URL_HOST ), // host URL.
'request_uri' => parse_url( $request_url, PHP_URL_PATH ), // endpoint URL.
'method' => $method,
);
if ( ! empty( $body ) ) {
$data['body'] = $body;
}
return hash_hmac( 'sha256', wp_json_encode( $data ), $token_secret );
}
protected function request_signature( $token_key, $token_secret, $timestamp, $nonce, $time_diff ) {
$local_time = $timestamp - $time_diff;
if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
return new WP_Error(
'invalid_signature',
__( 'Unable to send request to WooCommerce Shipping & Tax server. The timestamp generated for the signature is too old.', 'woocommerce-services' )
);
}
$normalized_request_string = join(
"\n",
array(
$token_key,
$timestamp,
$nonce,
)
) . "\n";
return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
}
private function get_guid() {
$guid = WC_Connect_Options::get_option( 'store_guid', false );
if ( false === $guid ) {
$guid = $this->generate_guid();
WC_Connect_Options::update_option( 'store_guid', $guid );
}
return $guid;
}
/**
* Generates a GUID.
* This code is based of a snippet found in https://github.com/alixaxel/phunction,
* which was referenced in http://php.net/manual/en/function.com-create-guid.php
*
* @return string
*/
private function generate_guid() {
return strtolower(
sprintf(
'%04X%04X-%04X-%04X-%04X-%04X%04X%04X',
mt_rand( 0, 65535 ),
mt_rand( 0, 65535 ),
mt_rand( 0, 65535 ),
mt_rand( 16384, 20479 ),
mt_rand( 32768, 49151 ),
mt_rand( 0, 65535 ),
mt_rand( 0, 65535 ),
mt_rand( 0, 65535 )
)
);
}
}
}