This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,67 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WC_Connect_Account_Settings {
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
/**
* @var WC_Connect_Payment_Methods_Store
*/
protected $payment_methods_store;
public function __construct(
WC_Connect_Service_Settings_Store $settings_store,
WC_Connect_Payment_Methods_Store $payment_methods_store
) {
$this->settings_store = $settings_store;
$this->payment_methods_store = $payment_methods_store;
}
public function get() {
$payment_methods_warning = false;
$payment_methods_success = $this->payment_methods_store->fetch_payment_methods_from_connect_server();
if ( ! $payment_methods_success ) {
$payment_methods_warning = __( 'There was a problem updating your saved credit cards.', 'woocommerce-services' );
}
$connection_owner = WC_Connect_Jetpack::get_connection_owner();
$connection_owner_wpcom_data = WC_Connect_Jetpack::get_connection_owner_wpcom_data();
$last_box_id = get_user_meta( get_current_user_id(), 'wc_connect_last_box_id', true );
$last_box_id = $last_box_id === 'individual' ? '' : $last_box_id;
$last_service_id = get_user_meta( get_current_user_id(), 'wc_connect_last_service_id', true );
$last_carrier_id = get_user_meta( get_current_user_id(), 'wc_connect_last_carrier_id', true );
$wcshipping_migration_state = intval( get_option( 'wcshipping_migration_state' ) );
return array(
'storeOptions' => $this->settings_store->get_store_options(),
'formData' => $this->settings_store->get_account_settings(),
'formMeta' => array(
'can_manage_payments' => $this->settings_store->can_user_manage_payment_methods(),
'can_edit_settings' => true,
'master_user_name' => $connection_owner ? $connection_owner->display_name : '',
'master_user_login' => $connection_owner ? $connection_owner->user_login : '',
'master_user_wpcom_login' => $connection_owner_wpcom_data ? $connection_owner_wpcom_data['login'] : '',
'master_user_email' => $connection_owner_wpcom_data ? $connection_owner_wpcom_data['email'] : '',
'payment_methods' => $this->payment_methods_store->get_payment_methods(),
'add_payment_method_url' => $this->payment_methods_store->get_add_payment_method_url(),
'warnings' => array( 'payment_methods' => $payment_methods_warning ),
'is_eligible_to_migrate' => $this->settings_store->is_eligible_for_migration(),
'wcshipping_migration_state' => $wcshipping_migration_state,
),
'userMeta' => array(
'last_box_id' => $last_box_id,
'last_service_id' => $last_service_id,
'last_carrier_id' => $last_carrier_id,
),
);
}
}
@@ -0,0 +1,125 @@
<?php
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'WOOCOMMERCE_CONNECT_SERVER_URL' ) ) {
define( 'WOOCOMMERCE_CONNECT_SERVER_URL', 'https://api.woocommerce.com/' );
}
if ( ! class_exists( 'WC_Connect_API_Client_Live' ) ) {
require_once plugin_basename( 'class-wc-connect-api-client.php' );
class WC_Connect_API_Client_Live extends WC_Connect_API_Client {
protected function request( $method, $path, $body = array() ) {
// TODO - incorporate caching for repeated identical requests
if ( ! is_array( $body ) ) {
return new WP_Error(
'request_body_should_be_array',
__( 'Unable to send request to WooCommerce Shipping & Tax server. Body must be an array.', 'woocommerce-services' )
);
}
$url = trailingslashit( WOOCOMMERCE_CONNECT_SERVER_URL );
$url = apply_filters( 'wc_connect_server_url', $url );
$url = trailingslashit( $url ) . ltrim( $path, '/' );
// Add useful system information to requests that contain bodies
if ( in_array( $method, array( 'POST', 'PUT' ) ) ) {
$body = $this->request_body( $body );
$body = wp_json_encode( apply_filters( 'wc_connect_api_client_body', $body ) );
if ( ! $body ) {
return new WP_Error(
'unable_to_json_encode_body',
__( 'Unable to encode body for request to WooCommerce Shipping & Tax server.', 'woocommerce-services' )
);
}
}
$headers = $this->request_headers();
if ( is_wp_error( $headers ) ) {
return $headers;
}
$http_timeout = 60; // 1 minute
if ( function_exists( 'wc_set_time_limit' ) ) {
wc_set_time_limit( $http_timeout + 10 );
}
$args = array(
'headers' => $headers,
'method' => $method,
'body' => $body,
'redirection' => 0,
'compress' => true,
'timeout' => $http_timeout,
);
$args = apply_filters( 'wc_connect_request_args', $args );
$response = wp_remote_request( $url, $args );
$response_code = wp_remote_retrieve_response_code( $response );
// If the received response is not JSON, return the raw response.
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
if ( false === strpos( $content_type, 'application/json' ) ) {
if ( 200 != $response_code ) {
return new WP_Error(
'wcc_server_error',
sprintf(
__( 'Error: The WooCommerce Shipping & Tax server returned HTTP code: %d', 'woocommerce-services' ),
$response_code
),
array(
'response_status_code' => $response_code,
)
);
}
return $response;
}
$response_body = wp_remote_retrieve_body( $response );
if ( ! empty( $response_body ) ) {
$response_body = json_decode( $response_body );
}
if ( 200 != $response_code ) {
if ( empty( $response_body ) ) {
return new WP_Error(
'wcc_server_empty_response',
sprintf(
__( 'Error: The WooCommerce Shipping & Tax server returned ( %d ) and an empty response body.', 'woocommerce-services' ),
$response_code
),
array(
'response_status_code' => $response_code,
)
);
}
$error = property_exists( $response_body, 'error' ) ? $response_body->error : '';
$message = property_exists( $response_body, 'message' ) ? $response_body->message : '';
$data = property_exists( $response_body, 'data' ) ? (array) $response_body->data : array();
$data['response_status_code'] = $response_code;
return new WP_Error(
'wcc_server_error_response',
sprintf(
/* translators: %1$s: error code, %2$s: error message, %3$d: HTTP response code */
__( 'Error: The WooCommerce Shipping & Tax server returned: %1$s %2$s ( %3$d )', 'woocommerce-services' ),
$error,
$message,
$response_code
),
$data
);
}
return $response_body;
}
}
}
@@ -0,0 +1,649 @@
<?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', array( 'settings' => array( 'wcship_migration_supported' => true ) ) );
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 )
)
);
}
}
}
@@ -0,0 +1,96 @@
<?php
/**
* Validates the shipping info in the cart.
*/
class WC_Connect_Cart_Validation {
/**
* Needed to keep track of the current package being processed.
*
* @var array
*/
private $current_package;
/**
* Register actions when required instead of using constructor.
*/
public function register_actions() {
add_action( 'woocommerce_store_api_cart_errors', [ $this, 'add_api_cart_errors' ], 10, 2 );
}
/**
* Register filters when required instead of using constructor.
*/
public function register_filters() {
add_filter( 'woocommerce_cart_no_shipping_available_html', [ $this, 'error_no_shipping_available_html' ] );
add_filter( 'woocommerce_shipping_package_name', [ $this, 'set_current_package' ], 10, 3 );
}
/**
* We use this filter to store the current package array because it's not passed to the woocommerce_cart_no_shipping_available_html filter.
*
* @param string $package_name Package Name.
* @param int $i Index.
* @param array $package Current package we need to store.
*
* @return mixed
*/
public function set_current_package( $package_name, $i, $package ) {
$this->current_package = $package;
return $package_name;
}
/**
* More friendly error message when WCS has an error.
*
* @param string $error_html Generic error message from WC.
* @return string
*/
public function error_no_shipping_available_html( $error_html ) {
foreach ( WC()->shipping()->load_shipping_methods( $this->current_package ) as $shipping_method ) {
if ( $shipping_method instanceof WC_Connect_Shipping_Method ) {
// We have to always force validation to run because WC_Shipping could cache package rates.
$shipping_method->is_valid_package_destination( $this->current_package );
$errors = $shipping_method->get_package_validation_errors();
if ( $errors->has_errors() ) {
return $errors->get_error_message();
}
}
}
return $error_html;
}
/**
* Check the error on the first load at Cart and Checkout page that has cart block or checkout block.
*
* @param \WP_Error $cart_errors List of errors in the cart.
* @param \WC_Cart $cart Cart object.
*/
public function add_api_cart_errors( $cart_errors, $cart ) {
if ( WC_Connect_Functions::is_store_api_call() ) {
return;
}
$all_notices = wc_get_notices();
$notices = array();
foreach ( $all_notices as $type => $type_notices ) {
if ( is_array( $type_notices ) && 'error' === $type ) {
$notices = array_merge( $notices, $type_notices );
}
}
$added_notices = array();
if ( ! empty( $notices ) ) {
$i = 1;
foreach ( $notices as $notice ) {
if ( ! in_array( $notice['notice'], $added_notices ) ) {
$added_notices[] = $notice['notice'];
$cart_errors->add( 'notice_' . $i, $notice['notice'] );
$i++;
}
}
}
}
}
@@ -0,0 +1,42 @@
<?php
/**
* A class for working around the quirks and different versions of WordPress/WooCommerce
* This is for versions higher than 2.6 (3.0 and higher)
*/
// No direct access please.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Compatibility_WC30' ) ) {
/**
* WC_Connect_Compatibility class.
*/
class WC_Connect_Compatibility_WC30 extends WC_Connect_Compatibility {
/**
* Return the order admin screen
*
* @return string The order admin screen
*/
public function get_order_admin_screen() {
return 'shop_order';
}
/**
* Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering.
*
* @param WC_Order|WP_Post $post_or_order_object Post or order object.
*
* @return bool|WC_Order|WC_Order_Refund
*/
public function init_theorder_object( $post_or_order_object ) {
if ( $post_or_order_object instanceof WC_Order ) {
return $post_or_order_object;
}
return wc_get_order( $post_or_order_object->ID );
}
}
}
@@ -0,0 +1,40 @@
<?php
/**
* A class for working around the quirks and different versions of WordPress/WooCommerce
* This is for versions 6.9 and higher
*/
use Automattic\WooCommerce\Utilities\OrderUtil;
// No direct access please.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Compatibility_WC69' ) ) {
/**
* WC_Connect_Compatibility class.
*/
class WC_Connect_Compatibility_WC69 extends WC_Connect_Compatibility {
/**
* Return the order admin screen
*
* @return string The order admin screen
*/
public function get_order_admin_screen(): string {
return OrderUtil::get_order_admin_screen();
}
/**
* Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering.
*
* @param WC_Order|WP_Post $post_or_order_object Post or order object.
*
* @return bool|WC_Order|WC_Order_Refund WC_Order object.
*/
public function init_theorder_object( $post_or_order_object ) {
return OrderUtil::init_theorder_object( $post_or_order_object );
}
}
}
@@ -0,0 +1,356 @@
<?php
/**
* Replaces saved package data with WooCommerce Shipping's if it is active.
*
* Redirects reads of and writes to wc_connect_options[packages] to wcshipping_options[packages].
*
* @since x.x.x
*/
class WC_Connect_Compatibility_WCShipping_Packages {
/**
* Number WCShipping uses to indicate data migration from WCS&T has completed.
*
* @var int
*/
const WCSHIP_DATA_MIGRATION_COMPLETED = 14;
/**
* Mapping of WCShipping keys => WCS&T keys.
*
* @var array
*/
const WCSHIPPING_TO_WCSERVICES_KEY_MAP = array(
'boxWeight' => 'box_weight',
'dimensions' => 'inner_dimensions',
'maxWeight' => 'max_weight',
);
/**
* Keys that are allowed in packages mapped to the WCS&T package data format.
*
* Other keys will be removed.
*
* @var array
*/
const KEYS_USED_BY_WCSERVICES = array(
'box_weight',
'inner_dimensions',
'is_letter',
'is_user_defined',
'max_weight',
'name',
);
/**
* Keys that are allowed in packages mapped to the WCShipping package data format.
*
* Other keys will be removed.
*
* @var array
*/
const KEYS_USED_BY_WCSHIPPING = array(
'boxWeight',
'dimensions',
'id',
'is_user_defined',
'maxWeight',
'name',
'type',
);
/**
* Registers all, some, or no hooks based on store configuration.
*
* @return void
*/
public static function maybe_enable() {
// Don't do anything if WooCommerce Shipping is not active.
if ( ! WC_Connect_Loader::is_wc_shipping_activated() ) {
return;
}
self::register_rest_controller_hooks();
$is_migration_to_wcshipping_completed = self::WCSHIP_DATA_MIGRATION_COMPLETED === (int) get_option( 'wcshipping_migration_state' );
if ( $is_migration_to_wcshipping_completed ) {
self::register_option_overwriting_hooks();
}
}
/**
* Enqueue REST controller registration after WCS&T has finished initializing its other controllers.
*
* @return void
*/
public static function register_rest_controller_hooks() {
add_action( 'wcservices_rest_api_init', array( self::class, 'register_wcshipping_compatibility_rest_controller' ) );
}
/**
* Registers hooks intercepting reads/writes to "wc_connect_options".
*
* This is done to replace the keys "packages" and "predefined_packages" with values from WCShipping's options
* after doing some mapping.
*
* @return void
*/
public static function register_option_overwriting_hooks() {
// Intercept reads of "wc_connect_options[packages]" and "wc_connect_options[predefined_packages]".
add_filter( 'option_wc_connect_options', array( self::class, 'intercept_packages_read' ) );
add_filter( 'option_wc_connect_options', array( self::class, 'intercept_predefined_packages_read' ) );
// Intercept updates to "wc_connect_options[packages]" and "wc_connect_options[predefined_packages]".
add_action( 'pre_update_option_wc_connect_options', array( self::class, 'intercept_packages_update' ), 10, 2 );
add_action( 'pre_update_option_wc_connect_options', array( self::class, 'intercept_predefined_packages_update' ), 10, 2 );
}
/**
* Replaces `wc_connect_options[packages]` with mapped values from `wcshipping_options[packages]`.
*
* Leaves the rest of `wc_connect_options` intact.
*
* @param mixed $wc_connect_options "wc_connect_options" value from the WP options table.
*
* @return mixed
*/
public static function intercept_packages_read( $wc_connect_options ) {
$wcshipping_options = get_option( 'wcshipping_options' );
if ( is_array( $wcshipping_options ) && isset( $wcshipping_options['packages'] ) ) {
$wc_connect_options['packages'] = self::map_packages_to_wcservices_format( $wcshipping_options['packages'] );
}
return $wc_connect_options;
}
/**
* Replaces `wc_connect_options[predefined_packages]` with values from `wcshipping_options[predefined_packages]`.
*
* Leaves the rest of `wc_connect_options` intact.
*
* @param mixed $wc_connect_options "wc_connect_options" value from the WP options table.
*
* @return mixed
*/
public static function intercept_predefined_packages_read( $wc_connect_options ) {
$wcshipping_options = get_option( 'wcshipping_options' );
if ( is_array( $wcshipping_options ) && isset( $wcshipping_options['predefined_packages'] ) ) {
$wc_connect_options['predefined_packages'] = $wcshipping_options['predefined_packages'];
}
return $wc_connect_options;
}
/**
* Saves the mapped value of `wc_connect_options[packages]` to `wcshipping_options[packages]`.
*
* Reverts `wc_connect_options[packages]` to old value so that only the packages
* in `wcshipping_options` get updated.
*
* Leaves the rest of `wcshipping_options` intact.
*
* @param mixed $value New value for "wc_connect_options" to extract packages from.
* @param mixed $old_value Old value of "wc_connect_options".
*
* @return array `$value` with the `packages` field reverted to current DB value to prevent updating.
*/
public static function intercept_packages_update( $value, $old_value ) {
$wcshipping_options = get_option( 'wcshipping_options' );
if ( ! empty( $value['packages'] ) ) {
$wcshipping_options['packages'] = self::map_packages_to_wcshipping_format( $value['packages'] );
} else {
$wcshipping_options['packages'] = array();
}
update_option( 'wcshipping_options', $wcshipping_options );
/*
* Prevent update of WCS&T's packages so that only `wcshipping_options` get updated.
*/
$value['packages'] = $old_value['packages'];
return $value;
}
/**
* Saves the mapped value of `wc_connect_options[predefined_packages]` to `wcshipping_options[predefined_packages]`.
*
* Reverts `wc_connect_options[predefined_packages]` to old value so that only the predefined packages
* in `wcshipping_options` get updated.
*
* Leaves the rest of `wcshipping_options` intact.
*
* @param mixed $value New value for "wc_connect_options" to extract predefined packages from.
* @param mixed $old_value Old value of "wc_connect_options".
*
* @return array `$value` with the `predefined_packages` field reverted to current DB value to prevent updating.
*/
public static function intercept_predefined_packages_update( $value, $old_value ) {
$wcshipping_options = get_option( 'wcshipping_options' );
if ( ! empty( $value['predefined_packages'] ) ) {
$wcshipping_options['predefined_packages'] = $value['predefined_packages'];
} else {
$wcshipping_options['predefined_packages'] = array();
}
update_option( 'wcshipping_options', $wcshipping_options );
/*
* Prevent update of WCS&T's predefined packages so that only `wcshipping_options` get updated.
*/
$value['predefined_packages'] = $old_value['predefined_packages'];
return $value;
}
/**
* Register a REST controller that reads "wc_connect_options".
*
* We do this because if WCShipping is active, it registers its own controller under /wc/v1/connect/packages
* that accesses "wcshipping_options". For the purpose of the WCS&T settings page, we still want the page
* accessing `wc_connect_options` that we'll possibly overwrite with the option read/write-intercepting filters
* if migration of options from WCS&T to WCShipping has been completed.
*
* This is so that we can always modify the value of "wc_connect_options" but leave the value of
* "wcshipping_options" intact.
*
* If migration has been completed, the controller will overwrite the value of "wc_connect_options[packages]" with
* WCShipping's packages.
*
* If migration hasn't been completed, it will return the value of "wc_connect_options[packages]" with no changes.
*
* @see self::register_option_overwriting_hooks
*
* @param WC_Connect_Loader $loader WCS&T's main class.
*/
public static function register_wcshipping_compatibility_rest_controller( WC_Connect_Loader $loader ) {
require_once __DIR__ . '/class-wc-rest-connect-wcshipping-compatibility-packages-controller.php';
$rest_wcshipping_package_compatibility_controller = new WC_REST_Connect_WCShipping_Compatibility_Packages_Controller(
$loader->get_api_client(),
$loader->get_service_settings_store(),
$loader->get_logger(),
$loader->get_service_schemas_store()
);
$rest_wcshipping_package_compatibility_controller->register_routes();
}
/**
* Maps package data from WCShipping's to WCS&T's format.
*
* @param array $custom_packages The custom packages to map from WCShipping's to WCS&T's format.
*
* @return array
*/
public static function map_packages_to_wcservices_format( $custom_packages ) {
$old_custom_packages = $custom_packages;
foreach ( $custom_packages as &$package ) {
$package = self::rename_keys( $package, self::WCSHIPPING_TO_WCSERVICES_KEY_MAP );
$package = self::map_type_to_is_letter( $package );
$package = self::unset_unused_keys( $package, self::KEYS_USED_BY_WCSERVICES );
}
return apply_filters(
'wcservices_map_packages_to_wcservices_format',
$custom_packages,
$old_custom_packages
);
}
/**
* Maps package data from WCS&T's to WCShipping's format.
*
* @param array $custom_packages The custom packages to map from WCS&T's to WCShipping's format.
*
* @return array
*/
public static function map_packages_to_wcshipping_format( $custom_packages ) {
$old_custom_packages = $custom_packages;
foreach ( $custom_packages as &$package ) {
$package = self::rename_keys( $package, array_flip( self::WCSHIPPING_TO_WCSERVICES_KEY_MAP ) );
$package = self::map_is_letter_to_type( $package );
$package = self::unset_unused_keys( $package, self::KEYS_USED_BY_WCSHIPPING );
}
return apply_filters(
'wcservices_map_packages_to_wcshipping_format',
$custom_packages,
$old_custom_packages
);
}
/**
* Renames keys according to provided key map then unsets the original keys.
*
* @param array $package Package data.
* @param array $key_map Mapping to follow.
*
* @return array
*/
private static function rename_keys( $package, $key_map ) {
foreach ( $key_map as $source => $target ) {
if ( isset( $package[ $source ] ) ) {
$package[ $target ] = $package[ $source ];
unset( $package[ $source ] );
}
}
return $package;
}
/**
* Unsets keys that aren't in `$allowed_keys`.
*
* @param array $package Package data.
* @param array $allowed_keys Keys that will be left in the array, if present. Other keys are unset.
*
* @return array
*/
private static function unset_unused_keys( $package, $allowed_keys ) {
return array_intersect_key( $package, array_flip( $allowed_keys ) );
}
/**
* Maps a package's "type" prop ("box"/"envelope") to "is_letter" (true/false).
*
* "type" is the format used by WCShipping.
* "is_letter" is the format used by WCS&T.
*
* @param array $package Package data.
*
* @return array
*/
private static function map_type_to_is_letter( $package ) {
if ( isset( $package['type'] ) ) {
$package['is_letter'] = 'envelope' === $package['type'];
}
unset( $package['type'] );
return $package;
}
/**
* Maps a package's "is_letter" prop (true/false) to "type" ("box"/"envelope").
*
* "type" is the format used by WCShipping.
* "is_letter" is the format used by WCS&T.
*
* @param array $package Package data.
*
* @return array
*/
private static function map_is_letter_to_type( $package ) {
if ( isset( $package['is_letter'] ) ) {
$package['type'] = $package['is_letter'] ? 'envelope' : 'box';
}
unset( $package['is_letter'] );
return $package;
}
}
@@ -0,0 +1,100 @@
<?php
/**
* A class for working around the quirks and different versions of WordPress/WooCommerce
* This is the base class. Its static members auto-select the correct version to use.
*/
// No direct access please.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Compatibility' ) ) {
/**
* WC_Connect_Compatibility class.
*/
abstract class WC_Connect_Compatibility {
/**
* WC_Connect_Compatibility.
*
* @var WC_Connect_Compatibility
*/
private static $singleton;
/**
* Woocommerce version.
*
* @var string
*/
private static $version = WC_VERSION;
/**
* WC_Connect_Compatibility singleton instance.
*
* @return WC_Connect_Compatibility
*/
public static function instance() {
if ( is_null( self::$singleton ) ) {
self::$singleton = self::select_compatibility();
}
return self::$singleton;
}
/**
* Return subclass for active version of WooCommerce.
*
* @return WC_Connect_Compatibility subclass for active version of WooCommerce
*/
private static function select_compatibility() {
if ( version_compare( self::$version, '6.9.0', '<' ) ) {
require_once 'class-wc-connect-compatibility-wc30.php';
return new WC_Connect_Compatibility_WC30();
} else {
require_once 'class-wc-connect-compatibility-wc69.php';
return new WC_Connect_Compatibility_WC69();
}
}
/**
* Overwrite default WooCommerce Version.
*
* @param string $value WooCommerce Version.
* @return void
*/
public static function set_version( $value ) {
self::$singleton = null;
self::$version = $value;
}
/**
* Revert to current WooCommerce Version.
*
* @return void
*/
public static function reset_version() {
self::$singleton = null;
self::$version = WC_VERSION;
}
/**
* Return the order admin screen
*
* @return string The order admin screen
*/
abstract public function get_order_admin_screen();
/**
* Helper function to initialize the global $theorder object, mostly used during order meta boxes rendering.
*
* @param WC_Order|WP_Post $post_or_order_object Post or order object.
*
* @return bool|WC_Order|WC_Order_Refund.
*/
abstract public function init_theorder_object( $post_or_order_object );
}
}
@@ -0,0 +1,108 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_Connect_Continents' ) ) {
return;
}
class WC_Connect_Continents {
/**
* Return the list of countries and states for a given continent.
*
* @since 3.1.0
* @param string $continent_code
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function get_continent( $continent_code = false ) {
$continents = WC()->countries->get_continents();
$countries = WC()->countries->get_countries();
$states = WC()->countries->get_states();
$locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';
$data = array();
if ( ! array_key_exists( $continent_code, $continents ) ) {
return false;
}
$continent_list = $continents[ $continent_code ];
$continent = array(
'code' => $continent_code,
'name' => $continent_list['name'],
);
$local_countries = array();
foreach ( $continent_list['countries'] as $country_code ) {
if ( isset( $countries[ $country_code ] ) ) {
$country = array(
'code' => $country_code,
'name' => $countries[ $country_code ],
);
// If we have detailed locale information include that in the response
if ( array_key_exists( $country_code, $locale_info ) ) {
// Defensive programming against unexpected changes in locale-info.php
$country_data = wp_parse_args(
$locale_info[ $country_code ],
array(
'currency_code' => 'USD',
'currency_pos' => 'left',
'decimal_sep' => '.',
'dimension_unit' => 'in',
'num_decimals' => 2,
'thousand_sep' => ',',
'weight_unit' => 'lbs',
)
);
$country = array_merge( $country_data, $country );
}
$local_states = array();
if ( isset( $states[ $country_code ] ) ) {
foreach ( $states[ $country_code ] as $state_code => $state_name ) {
$local_states[] = array(
'code' => $state_code,
'name' => $state_name,
);
}
}
$country['states'] = $local_states;
// Allow only desired keys (e.g. filter out tax rates)
$allowed = array(
'code',
'currency_code',
'currency_pos',
'decimal_sep',
'dimension_unit',
'name',
'num_decimals',
'states',
'thousand_sep',
'weight_unit',
);
$country = array_intersect_key( $country, array_flip( $allowed ) );
$local_countries[] = $country;
}
}
$continent['countries'] = $local_countries;
return $continent;
}
public function get() {
$continents = array();
foreach ( array_keys( WC()->countries->get_continents() ) as $continent_code ) {
$continents[] = $this->get_continent( $continent_code, null );
}
return $continents;
}
}
@@ -0,0 +1,127 @@
<?php
/**
* A class for custom surcharge.
*/
// No direct access please.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Custom_Surcharge' ) ) {
/**
* WC_Connect_Custom_Surcharge class.
*/
class WC_Connect_Custom_Surcharge {
/**
* Initialize the class and set up action hooks.
*/
public static function init() {
add_action( 'woocommerce_cart_calculate_fees', array( static::class, 'add_us_co_retail_delivery_fee' ), 10 );
}
/**
* Add US Colorado Retail Delivery Fee Tax.
* Uses the WooCommerce fees API `$cart->add_fee()`.
*
* Colorado Retail Delivery Fee Tax:
* https://www.avalara.com/blog/en/north-america/2022/10/what-you-need-to-know-about-the-colorado-retail-delivery-fee-now.html
*
* RDF fee is DISABLED by default - not all business are required to charge the fee.
* To apply the fee use `wc_services_apply_us_co_retail_delivery_fee` filter.
* Change boolian flag to `true`
* Example: `add_filter( 'wc_services_apply_us_co_retail_delivery_fee', '__return_true' );`
*
* @param WC_Cart $cart WooCommerce Cart object.
*/
public static function add_us_co_retail_delivery_fee( $cart ) {
/**
* Filter should Retail Delivery Fee be applied.
* Default: false.
*
* @since 2.9.0
*
* @param bool Should the Retail Delivery Fee be applied.
* @param WC_Cart WooCommerce cart object.
*/
if ( ! apply_filters( 'wc_services_enable_us_co_retail_delivery_fee', false, $cart ) ) {
return;
}
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
if ( false === ( $cart instanceof WC_Cart ) ) {
return;
}
// Do not apply RDF if the customer is not in US Colorado.
if (
'US' !== WC()->customer->get_shipping_country()
|| 'CO' !== WC()->customer->get_shipping_state()
) {
return;
}
// Do not apply RDF when every item in order is exempt from Colorado sales tax.
if (
! is_array( $cart->get_cart_contents_taxes() )
|| 0 === count( $cart->get_cart_contents_taxes() )
) {
return;
}
// Do not apply RDF if all shipping methods use Local Pickup.
if ( 0 === count(
array_diff(
wc_get_chosen_shipping_method_ids(),
/**
* Filters local pickup shipping methods.
* Copied from WooCommerce core to maintain compatability.
*
* @since 6.8.0
* @param string[] $local_pickup_methods Local pickup shipping method IDs.
*/
apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) )
)
) ) {
return;
}
// Do not apply RDF if all products are virtual.
if ( ! $cart->needs_shipping() ) {
return;
}
/**
* Filter for manipulate the custom surcharge.
*
* As of July 1, 2024 till June 30, 2025 RDF is 29 cents per order
* RDF is subject to sales tax.
* https://www.avalara.com/blog/en/north-america/2022/10/what-you-need-to-know-about-the-colorado-retail-delivery-fee-now.html.
*
* @since 2.9.0
*
* @param array Custom surcharge info.
* @param WC_Cart WooCommerce cart object.
*/
$fee_info = apply_filters(
'wc_services_apply_us_co_retail_delivery_fee',
array(
'value' => 0.29,
'text' => __( 'Retail Delivery Fee', 'woocommerce_services' ),
),
$cart
);
if (
! empty( $fee_info['text'] ) &&
isset( $fee_info['value'] ) && is_numeric( $fee_info['value'] )
) {
$cart->add_fee( $fee_info['text'], floatval( $fee_info['value'] ), true, 'standard' );
}
}
}
}
@@ -0,0 +1,149 @@
<?php
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Debug_Tools' ) ) {
class WC_Connect_Debug_Tools {
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
function __construct( WC_Connect_API_Client $api_client ) {
$this->api_client = $api_client;
add_filter( 'woocommerce_debug_tools', array( $this, 'woocommerce_debug_tools' ) );
}
function woocommerce_debug_tools( $tools ) {
$tools['test_wcc_connection'] = array(
'name' => __( 'Test your WooCommerce Shipping & Tax connection', 'woocommerce-services' ),
'button' => __( 'Test Connection', 'woocommerce-services' ),
'desc' => __( 'This will test your WooCommerce Shipping & Tax connection to ensure everything is working correctly', 'woocommerce-services' ),
'callback' => array( $this, 'test_connection' ),
);
/**
* Only show this tool for stores not based in California
*/
if ( 'CA' !== WC()->countries->get_base_state() ) {
$tools['delete_ca_taxes'] = array(
'name' => __( 'Delete California tax rates', 'woocommerce-services' ),
'button' => __( 'Delete CA tax rates', 'woocommerce-services' ),
'desc' => sprintf( '<strong class="red">%1$s</strong> %2$s %3$s %4$s <a href="https://woocommerce.com/document/woocommerce-shipping-and-tax/woocommerce-tax/#jan-2022-ca-notice" target="_blank">%5$s</a>', __( 'Note:', 'woocommerce-services' ), __( 'This option will delete ALL of your "CA" tax rates where the tax name ends with " Tax" (case insensitive).', 'woocommerce-services' ), '<br>', __( 'A backup CSV of all existing tax rates will be made before the deletion process runs.', 'woocommerce-services' ), __( 'Additional information.', 'woocommerce-services' ) ),
'callback' => array( $this, 'delete_california_tax_rates' ),
);
}
/**
* Only show when object cache is disabled - the tool doesn't work when object cache is enabled.
*/
if ( ! wp_using_ext_object_cache() ) {
$tools['delete_cached_tax_server_responses'] = array(
'name' => __( 'Delete WooCommerce Tax cached tax rate responses', 'woocommerce-services' ),
'button' => __( 'Delete cached Tax transients', 'woocommerce-services' ),
'desc' => __( 'Deletes the all the transients in your database that represent cached Tax Rates responses', 'woocommerce-services' ),
'callback' => array( $this, 'delete_cached_tax_server_responses' ),
);
}
return $tools;
}
function test_connection() {
$test_request = $this->api_client->auth_test();
if ( $test_request && ! is_wp_error( $test_request ) && $test_request->authorized ) {
echo '<div class="updated inline"><p>' . esc_html__( 'Your site is successfully communicating to the WooCommerce Shipping & Tax API.', 'woocommerce-services' ) . '</p></div>';
} else {
echo '<div class="error inline"><p>'
. esc_html__( 'ERROR: Your site has a problem connecting to the WooCommerce Shipping & Tax API. Please make sure your WordPress.com connection is working.', 'woocommerce-services' )
. '</p></div>';
}
}
/**
* Back up all existing tax rates from the database in a CSV file.
* Then, if successfully backed up, loop through the tax rates
* in the database and delete rates where:
* tax_rate_country = 'US' and
* tax_rate_state = 'CA' and
* tax_rate_name LIKE '% Tax'
*
* @return void
*/
function delete_california_tax_rates() {
$backed_up = WC_Connect_Functions::backup_existing_tax_rates();
if ( ! $backed_up ) {
echo '<div class="error inline"><p>';
echo esc_html__( 'ERROR: The "CA" tax rate deletion process was cancelled because the existing tax rates could not be backed up.', 'woocommerce-services' );
echo '</p></div>';
return;
}
global $wpdb;
$found_ca_rates = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates
WHERE tax_rate_country = %s AND tax_rate_state = %s AND tax_rate_name LIKE %s
",
'US',
'CA',
'% Tax'
),
ARRAY_A
);
/**
* If no rates were found, output a message and return
*/
if ( empty( $found_ca_rates ) ) {
echo '<div class="updated inline"><p>';
echo esc_html__( 'No "CA" tax rates were found.', 'woocommerce-services' );
echo '</p></div>';
return;
}
$deleted_count = 0;
foreach ( $found_ca_rates as $rate ) {
if ( empty( $rate['tax_rate_id'] ) ) {
continue;
}
WC_Tax::_delete_tax_rate( $rate['tax_rate_id'] );
$deleted_count ++;
}
echo '<div class="updated inline"><p>';
echo sprintf( esc_html__( 'Successfully deleted %1$d rows from the database.', 'woocommerce-services' ), intval( $deleted_count ) );
echo '</p></div>';
}
/**
* Deletes the all the transients in the database that represent cached Tax Rates responses.
*
* @return void
*/
function delete_cached_tax_server_responses() {
global $wpdb;
$deleted_count = absint(
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '%tj\_tax\_%';"
)
);
echo '<div class="updated inline"><p>';
echo sprintf( esc_html__( 'Successfully deleted %1$d transients from the database.', 'woocommerce-services' ), intval( $deleted_count ) );
echo '</p></div>';
}
}
}
@@ -0,0 +1,111 @@
<?php
/**
* Show admin notices when errors occur
*/
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Error_Notice' ) ) {
class WC_Connect_Error_Notice {
private static $inst = null;
public static function instance() {
if ( null === self::$inst ) {
self::$inst = new WC_Connect_Error_Notice();
}
return self::$inst;
}
public function enable_notice( $error = true ) {
WC_Connect_Options::update_option( 'error_notice', $error );
}
public function disable_notice() {
WC_Connect_Options::update_option( 'error_notice', false );
}
public function render_notice() {
$error_notice = filter_input( INPUT_GET, 'wc-connect-error-notice', FILTER_SANITIZE_ENCODED );
if ( 'disable' === $error_notice ) {
WC_Connect_Options::update_option( 'error_notice', false );
$url = remove_query_arg( 'wc-connect-error-notice' );
wp_safe_redirect( $url );
exit;
}
if ( $this->notice_enabled() ) {
$this->show_notice();
}
}
private function notice_enabled() {
return WC_Connect_Options::get_option( 'error_notice', false );
}
private function show_notice() {
$link_status = admin_url( 'admin.php?page=wc-status&tab=connect' );
$link_dismiss = add_query_arg( array( 'wc-connect-error-notice' => 'disable' ) );
$error = $this->notice_enabled();
if ( ! is_wp_error( $error ) ) {
return;
}
$message = false;
if (
'product_missing_weight' === $error->get_error_code() ||
'product_missing_dimension' === $error->get_error_code()
) {
$error_data = $error->get_error_data();
$id = $error_data['product_id'];
$product = wc_get_product( $id );
if (
! $product ||
( $product->has_weight() &&
$product->get_length() &&
$product->get_height() &&
$product->get_width()
)
) {
$this->disable_notice();
return;
}
$product_name = $product->get_name();
$product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
$message = sprintf(
__( '<strong>"%2$s" is missing weight, length, width, or height.</strong><br />Shipping rates cannot be calculated. <a href="%1$s">Enter dimensions and weight for %2$s</a> so your customers can purchase this item.', 'woocommerce-services' ),
get_edit_post_link( $product_id ),
$product_name
);
}
if ( ! $message ) {
return;
}
$allowed_html = array(
'a' => array( 'href' => array() ),
'strong' => array(),
'br' => array(),
);
?>
<div class='notice notice-error' style="position: relative;">
<a href="<?php echo esc_url( $link_dismiss ); ?>" style="text-decoration: none;" class="notice-dismiss" title="<?php esc_attr_e( 'Dismiss this notice', 'woocommerce-services' ); ?>"></a>
<p><?php echo wp_kses( $message, $allowed_html ); ?></p>
</div>
<?php
echo '';
}
}
}
@@ -0,0 +1,41 @@
<?php
if ( ! class_exists( 'WC_Connect_Extension_Compatibility' ) ) {
class WC_Connect_Extension_Compatibility {
/**
* Function called when a new tracking number is added to the order
*
* @param $order_id - order ID
* @param $carrier_id - carrier ID, as returned on the label objects returned by the server
* @param $tracking_number - tracking number string
*/
public static function on_new_tracking_number( $order_id, $carrier_id, $tracking_number ) {
// call WooCommerce Shipment Tracking if it's installed
if ( function_exists( 'wc_st_add_tracking_number' ) ) {
// note: the only carrier ID we use at the moment is 'usps', which is the same in WC_ST, but this might require a mapping
wc_st_add_tracking_number( $order_id, $tracking_number, $carrier_id );
}
}
/**
* Checks if WooCommerce Shipping & Tax should email the tracking details, or if another extension is taking care of that already
*
* @param $order_id - order ID
* @return boolean true if WCS should send the tracking info, false otherwise
*/
public static function should_email_tracking_details( $order_id ) {
if ( function_exists( 'wc_shipment_tracking' ) ) {
$shipment_tracking = wc_shipment_tracking();
if ( property_exists( $shipment_tracking, 'actions' )
&& method_exists( $shipment_tracking->actions, 'get_tracking_items' ) ) {
$shipment_tracking_items = $shipment_tracking->actions->get_tracking_items( $order_id );
if ( ! empty( $shipment_tracking_items ) ) {
return false;
}
}
}
return true;
}
}
}
@@ -0,0 +1,290 @@
<?php
if ( ! class_exists( 'WC_Connect_Functions' ) ) {
class WC_Connect_Functions {
/**
* Checks if the potentially expensive Shipping/Tax API requests should be sent
* based on the context in which they are initialized.
*
* @return bool true if the request can be sent, false otherwise
*/
public static function should_send_cart_api_request() {
// Allow if this is an API call to store/cart endpoint. Provides compatibility with WooCommerce Blocks.
return self::is_store_api_call() || ! (
// Skip for carts loaded from session in the dashboard.
( is_admin() && did_action( 'woocommerce_cart_loaded_from_session' ) ) ||
// Skip during Jetpack API requests.
( ! empty( $_SERVER['REQUEST_URI'] ) && false !== strpos( $_SERVER['REQUEST_URI'], 'jetpack/v4/' ) ) || // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
// Skip during REST API or XMLRPC requests.
( defined( 'REST_REQUEST' ) || defined( 'REST_API_REQUEST' ) || defined( 'XMLRPC_REQUEST' ) ) ||
// Skip during Jetpack REST API proxy requests.
( isset( $_GET['rest_route'] ) && isset( $_GET['_for'] ) && ( 'jetpack' === $_GET['_for'] ) )
);
}
/**
* Get the WC Helper authorization information to use with WC Connect Server requests( e.g. site ID, access token).
*
* @return array|WP_Error
*/
public static function get_wc_helper_auth_info() {
if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) {
$helper_auth_data = WC_Helper_Options::get( 'auth' );
}
// It's possible for WC_Helper_Options::get() to return false, throw error if this is the case.
if ( ! $helper_auth_data ) {
return new WP_Error(
'missing_wccom_auth',
__( 'WooCommerce Helper auth is missing', 'woocommerce-services' )
);
}
return $helper_auth_data;
}
/**
* Check if we are currently in Rest API request for the wc/store/cart or wc/store/checkout API call.
*
* @return bool
*/
public static function is_store_api_call() {
if ( ! WC()->is_rest_api_request() && empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
return false;
}
$rest_route = $GLOBALS['wp']->query_vars['rest_route'];
// Use regex to check any route that has "wc/store" with any of these text : "cart", "checkout", or "batch"
// Example : wc/store/v3/batch
preg_match( '/wc\/store\/v[0-9]{1,}\/(batch|cart|checkout)/', $rest_route, $route_matches, PREG_OFFSET_CAPTURE );
return ( ! empty( $route_matches ) );
}
/**
* Check if current page is a cart page or has woocommerce cart block.
*
* @return bool
*/
public static function is_cart() {
if ( is_cart() || self::has_cart_block() ) {
return true;
}
return false;
}
/**
* Check if current page is a checkout page or has woocommerce checkout block.
*
* @return bool
*/
public static function is_checkout() {
if ( is_checkout() || self::has_checkout_block() ) {
return true;
}
return false;
}
/**
* Check if current page has woocommerce cart block.
*
* @return bool
*/
public static function has_cart_block() {
// To support WP < 5.0.0, we need to check if `has_block` exists first as has_block only being introduced on WP 5.0.0.
if ( function_exists( 'has_block' ) ) {
return has_block( 'woocommerce/cart' );
}
return false;
}
/**
* Check if current page has woocommerce checkout block.
*
* @return bool
*/
public static function has_checkout_block() {
// To support WP < 5.0.0, we need to check if `has_block` exists first as has_block only being introduced on WP 5.0.0.
if ( function_exists( 'has_block' ) ) {
return has_block( 'woocommerce/checkout' );
}
return false;
}
/**
* Check if current page has woocommerce cart or checkout block.
*
* @return bool
*/
public static function has_cart_or_checkout_block() {
if ( self::has_checkout_block() || self::has_cart_block() ) {
return true;
}
return false;
}
/**
* Checks whether the current user has permissions to manage shipping labels.
*
* @return boolean
*/
public static function user_can_manage_labels() {
/**
* @since 1.25.14
*/
return apply_filters( 'wcship_user_can_manage_labels', current_user_can( 'manage_woocommerce' ) || current_user_can( 'wcship_manage_labels' ) );
}
/**
* Exports existing tax rates to a CSV and clears the table.
*
* Ported from TaxJar's plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/42cd4cd0/taxjar-woocommerce.php#L75
*
* @return boolean
*/
public static function backup_existing_tax_rates() {
global $wpdb;
// Export Tax Rates
$rates = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates
ORDER BY tax_rate_order
LIMIT %d, %d
",
0,
10000
)
);
ob_start();
$header =
__( 'Country Code', 'woocommerce' ) . ',' .
__( 'State Code', 'woocommerce' ) . ',' .
__( 'ZIP/Postcode', 'woocommerce' ) . ',' .
__( 'City', 'woocommerce' ) . ',' .
__( 'Rate %', 'woocommerce' ) . ',' .
__( 'Tax Name', 'woocommerce' ) . ',' .
__( 'Priority', 'woocommerce' ) . ',' .
__( 'Compound', 'woocommerce' ) . ',' .
__( 'Shipping', 'woocommerce' ) . ',' .
__( 'Tax Class', 'woocommerce' ) . "\n";
echo $header; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
foreach ( $rates as $rate ) {
if ( $rate->tax_rate_country ) {
echo esc_attr( $rate->tax_rate_country );
} else {
echo '*';
}
echo ',';
if ( $rate->tax_rate_state ) {
echo esc_attr( $rate->tax_rate_state );
} else {
echo '*';
}
echo ',';
$locations = $wpdb->get_col( $wpdb->prepare( "SELECT location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type='postcode' AND tax_rate_id = %d ORDER BY location_code", $rate->tax_rate_id ) );
if ( $locations ) {
echo esc_attr( implode( '; ', $locations ) );
} else {
echo '*';
}
echo ',';
$locations = $wpdb->get_col( $wpdb->prepare( "SELECT location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type='city' AND tax_rate_id = %d ORDER BY location_code", $rate->tax_rate_id ) );
if ( $locations ) {
echo esc_attr( implode( '; ', $locations ) );
} else {
echo '*';
}
echo ',';
if ( $rate->tax_rate ) {
echo esc_attr( $rate->tax_rate );
} else {
echo '0';
}
echo ',';
if ( $rate->tax_rate_name ) {
echo esc_attr( $rate->tax_rate_name );
} else {
echo '*';
}
echo ',';
if ( $rate->tax_rate_priority ) {
echo esc_attr( $rate->tax_rate_priority );
} else {
echo '1';
}
echo ',';
if ( $rate->tax_rate_compound ) {
echo esc_attr( $rate->tax_rate_compound );
} else {
echo '0';
}
echo ',';
if ( $rate->tax_rate_shipping ) {
echo esc_attr( $rate->tax_rate_shipping );
} else {
echo '0';
}
echo ',';
echo "\n";
} // End foreach().
$csv = ob_get_clean();
$upload_dir = wp_upload_dir();
$backed_up = file_put_contents( $upload_dir['basedir'] . '/taxjar-wc_tax_rates-' . date( 'm-d-Y' ) . '-' . time() . '.csv', $csv );
return (bool) $backed_up;
}
/**
* Search the uploads directory and return all backed up
* tax rate files.
*
* @return array|false
*/
public static function get_backed_up_tax_rate_files() {
$upload_dir = wp_upload_dir();
$pattern = $upload_dir['basedir'] . '/taxjar-wc_tax_rates-*.csv';
$found_files = glob( $pattern );
if ( empty( $found_files ) ) {
return false;
}
$files = [];
foreach ( $found_files as $file ) {
$filename = basename( $file );
$files[ $filename ] = $upload_dir['baseurl'] . '/' . $filename;
}
return $files;
}
}
}
@@ -0,0 +1,356 @@
<?php
if ( ! class_exists( 'WC_Connect_Help_View' ) ) {
class WC_Connect_Help_View {
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $service_settings_store;
/**
* @var WC_Connect_Logger
*/
protected $logger;
/**
* @var WC_Connect_TaxJar_Integration
*/
protected $taxjar_integration;
/**
* @array
*/
protected $fieldsets;
public function __construct(
WC_Connect_Service_Schemas_Store $service_schemas_store,
WC_Connect_TaxJar_Integration $taxjar_integration,
WC_Connect_Service_Settings_Store $service_settings_store,
WC_Connect_Logger $logger
) {
$this->service_schemas_store = $service_schemas_store;
$this->service_settings_store = $service_settings_store;
$this->logger = $logger;
$this->taxjar_integration = $taxjar_integration;
add_filter( 'woocommerce_admin_status_tabs', array( $this, 'status_tabs' ) );
add_action( 'woocommerce_admin_status_content_connect', array( $this, 'page' ) );
}
protected function get_health_items() {
$health_items = array();
// WooCommerce
// Only one of the following should present
// Check that WooCommerce is at least 2.6 or higher (feature-plugin only)
// Check that WooCommerce base_country is set
$base_country = WC()->countries->get_base_country();
if ( version_compare( WC()->version, WOOCOMMERCE_CONNECT_MINIMUM_WOOCOMMERCE_VERSION, '<' ) ) {
$health_item = array(
'state' => 'error',
'message' => sprintf(
__( 'WooCommerce %1$s or higher is required (You are running %2$s)', 'woocommerce-services' ),
WOOCOMMERCE_CONNECT_MINIMUM_WOOCOMMERCE_VERSION,
WC()->version
),
);
} elseif ( empty( $base_country ) ) {
$health_item = array(
'state' => 'error',
'message' => __( 'Please set Base Location in WooCommerce Settings > General', 'woocommerce-services' ),
);
} else {
$health_item = array(
'state' => 'success',
'message' => sprintf(
__( 'WooCommerce %s is configured correctly', 'woocommerce-services' ),
WC()->version
),
);
}
$health_items['woocommerce'] = $health_item;
if ( WC_Connect_Jetpack::is_offline_mode() ) {
$health_item = array(
'state' => 'warning',
'message' => __( 'This site is working in offline mode. This mode is activated when running the site on a local machine or if developer mode is enabled', 'woocommerce-services' ),
);
} elseif ( ! WC_Connect_Jetpack::is_connected() ) {
$health_item = array(
'state' => 'error',
'message' => __( 'Not connected to WordPress.com', 'woocommerce-services' ),
);
} elseif ( WC_Connect_Jetpack::is_staging_site() ) {
$health_item = array(
'state' => 'warning',
'message' => __( 'This site was identified as a staging site', 'woocommerce-services' ),
);
} else {
$health_item = array(
'state' => 'success',
'message' => __( 'Connected to WordPress.com', 'woocommerce-services' ),
);
}
$health_items['wpcom_connection'] = $health_item;
// Automated taxes status
$health_items['automated_taxes'] = $this->get_tax_health_item();
// Lastly, do the WooCommerce Shipping & Tax health check
// Check that we have schema
// Check that we are able to talk to the WooCommerce Shipping & Tax server
$schemas = $this->service_schemas_store->get_service_schemas();
$last_fetch_timestamp = $this->service_schemas_store->get_last_fetch_timestamp();
$health_items['woocommerce_services'] = array(
'timestamp' => $last_fetch_timestamp,
'has_service_schemas' => ! is_null( $schemas ),
'error_threshold' => 3 * DAY_IN_SECONDS,
'warning_threshold' => DAY_IN_SECONDS,
);
return $health_items;
}
protected function is_shipping_loaded() {
return ! in_array( 'woocommerce-shipping/woocommerce-shipping.php', get_option( 'active_plugins' ) );
}
protected function get_services_items() {
$available_service_method_ids = $this->service_schemas_store->get_all_shipping_method_ids();
if ( empty( $available_service_method_ids ) ) {
return false;
}
$service_items = array();
$enabled_services = $this->service_settings_store->get_enabled_services();
foreach ( (array) $enabled_services as $enabled_service ) {
$last_failed_request_timestamp = intval( WC_Connect_Options::get_shipping_method_option( 'failure_timestamp', -1, $enabled_service->method_id, $enabled_service->instance_id ) );
$service_settings_url = esc_url(
add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'shipping',
'instance_id' => $enabled_service->instance_id,
),
admin_url( 'admin.php' )
)
);
// Figure out if the service has any settings saved at all
$service_settings = $this->service_settings_store->get_service_settings( $enabled_service->method_id, $enabled_service->instance_id );
if ( empty( $service_settings ) ) {
$state = 'error';
$message = __( 'Setup for this service has not yet been completed', 'woocommerce-services' );
} elseif ( -1 === $last_failed_request_timestamp ) {
$state = 'warning';
$message = __( 'No rate requests have yet been made for this service', 'woocommerce-services' );
} elseif ( 0 === $last_failed_request_timestamp ) {
$state = 'success';
$message = __( 'The most recent rate request was successful', 'woocommerce-services' );
} else {
$state = 'error';
$message = __( 'The most recent rate request failed', 'woocommerce-services' );
}
$subtitle = sprintf(
__( '%s Shipping Zone', 'woocommerce-services' ),
$enabled_service->zone_name
);
$service_items[] = (object) array(
'title' => $enabled_service->title,
'subtitle' => $subtitle,
'state' => $state,
'message' => $message,
'timestamp' => $last_failed_request_timestamp,
'url' => $service_settings_url,
);
}
return $service_items;
}
/**
* Gets the last 10 lines from the WooCommerce Shipping & Tax log by feature, if it exists
*/
protected function get_debug_log_data( $feature = '' ) {
$data = new stdClass();
$data->key = '';
$data->file = null;
$data->tail = array();
if ( ! method_exists( 'WC_Admin_Status', 'scan_log_files' ) ) {
return $data;
}
$log_prefix = 'wc\-services';
if ( ! empty( $feature ) ) {
$log_prefix .= '\-' . $feature;
}
$logs = WC_Admin_Status::scan_log_files();
$latest_file_date = 0;
foreach ( $logs as $log_key => $log_file ) {
if ( ! preg_match( '/' . $log_prefix . '\-(?:\d{4}\-\d{2}\-\d{2}\-)?[0-9a-f]{32}\-log/', $log_key ) ) {
continue;
}
$log_file_path = WC_LOG_DIR . $log_file;
$file_date = filemtime( $log_file_path );
if ( $latest_file_date < $file_date ) {
$latest_file_date = $file_date;
$data->file = $log_file_path;
$data->key = $log_key;
}
}
if ( null !== $data->file ) {
$complete_log = file( $data->file );
$data->tail = array_slice( $complete_log, -10 );
}
$line_count = count( $data->tail );
if ( $line_count < 1 ) {
$log_tail = array( __( 'Log is empty', 'woocommerce-services' ) );
} else {
$log_tail = $data->tail;
}
return array(
'tail' => implode( $log_tail ),
'url' => $url = add_query_arg(
array(
'page' => 'wc-status',
'tab' => 'logs',
'log_file' => $data->key,
),
admin_url( 'admin.php' )
),
'count' => $line_count,
);
}
/**
* Filters the WooCommerce System Status Tabs to add connect
*
* @param array $tabs
* @return array
*/
public function status_tabs( $tabs ) {
if ( ! is_array( $tabs ) ) {
$tabs = array();
}
$tabs['connect'] = _x( 'WooCommerce Shipping & Tax', 'The WooCommerce Shipping & Tax brandname', 'woocommerce-services' );
return $tabs;
}
/**
* Returns the data bootstrap for the help page
*
* @return array
*/
protected function get_form_data() {
return array(
'health_items' => $this->get_health_items(),
'services' => $this->get_services_items(),
'logging_enabled' => $this->logger->is_logging_enabled(),
'debug_enabled' => $this->logger->is_debug_enabled(),
'logs' => array(
'shipping' => $this->get_debug_log_data( 'shipping' ),
'taxes' => $this->get_debug_log_data( 'taxes' ),
'other' => $this->get_debug_log_data(),
),
'tax_rate_backups' => WC_Connect_Functions::get_backed_up_tax_rate_files(),
'is_shipping_loaded' => $this->is_shipping_loaded(),
);
}
/**
* Localizes the bootstrap, enqueues the script and styles for the help page
*/
public function page() {
?>
<h2>
<?php esc_html_e( 'WooCommerce Shipping & Tax Status', 'woocommerce-services' ); ?>
</h2>
<?php
do_action(
'enqueue_wc_connect_script',
'wc-connect-admin-status',
array(
'formData' => $this->get_form_data(),
)
);
do_action(
'enqueue_wc_connect_script',
'wc-connect-admin-test-print',
array(
'isShippingLoaded' => $this->is_shipping_loaded(),
'storeOptions' => $this->service_settings_store->get_store_options(),
'paperSize' => $this->service_settings_store->get_preferred_paper_size(),
)
);
}
/**
* @return array
*/
protected function get_tax_health_item() {
$store_country = WC()->countries->get_base_country();
if ( ! $this->taxjar_integration->is_supported_country( $store_country ) ) {
return array(
'state' => 'error',
'settings_link_type' => '',
'message' => sprintf( __( 'Your store\'s country (%s) is not supported. Automated taxes functionality is disabled', 'woocommerce-services' ), $store_country ),
);
}
if ( class_exists( 'WC_Taxjar' ) ) {
return array(
'state' => 'error',
'settings_link_type' => '',
'message' => __( 'TaxJar extension detected. Automated taxes functionality is disabled', 'woocommerce-services' ),
);
}
if ( ! wc_tax_enabled() ) {
return array(
'state' => 'error',
'settings_link_type' => 'general',
'message' => __( 'The core WooCommerce taxes functionality is disabled. Please ensure the "Enable tax rates and calculations" setting is turned "on" in the WooCommerce settings page', 'woocommerce-services' ),
);
}
if ( ! $this->taxjar_integration->is_enabled() ) {
return array(
'state' => 'error',
'settings_link_type' => 'tax',
'message' => __( 'The automated taxes functionality is disabled. Enable the "Automated taxes" setting on the WooCommerce settings page', 'woocommerce-services' ),
);
}
return array(
'state' => 'success',
'settings_link_type' => 'tax',
'message' => __( 'Automated taxes are enabled', 'woocommerce-services' ),
);
}
}
}
@@ -0,0 +1,170 @@
<?php
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Connection\Package_Version;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
if ( ! class_exists( 'WC_Connect_Jetpack' ) ) {
class WC_Connect_Jetpack {
const JETPACK_PLUGIN_SLUG = 'woocommerce-services';
public static function get_connection_manager() {
return new Manager( self::JETPACK_PLUGIN_SLUG );
}
/**
* Returns the Blog Token.
*
* @return stdClass|WP_Error
*/
public static function get_blog_access_token() {
return self::get_connection_manager()->get_tokens()->get_access_token();
}
/**
* Helper method to get if Jetpack is in offline mode
*
* @return bool
*/
public static function is_offline_mode() {
$status = new Status();
return $status->is_offline_mode();
}
/**
* Helper method to get if Jetpack is connected (aka active).
*
* @deprecated 2.3.0 Use self::is_connected() instead.
*
* @return bool
*/
public static function is_active() {
return self::is_connected();
}
/**
* Helper method to get if the current Jetpack website is marked as staging
*
* @return bool
*/
public static function is_staging_site() {
$jetpack_status = new Status();
return $jetpack_status->in_safe_mode();
}
/**
* Helper method to get whether the current site is an Atomic site
*
* @return bool
*/
public static function is_atomic_site() {
return ( new Host() )->is_woa_site();
}
public static function get_connection_owner_wpcom_data() {
$connection_owner = self::get_connection_owner();
if ( ! $connection_owner ) {
return false;
}
return self::get_connection_manager()->get_connected_user_data( $connection_owner->ID );
}
/**
* Helper method to get the Jetpack connection owner, IF we are connected
*
* @return WP_User | false
*/
public static function get_connection_owner() {
if ( ! self::is_connected() ) {
return false;
}
return self::get_connection_manager()->get_connection_owner();
}
/**
* Records a Tracks event
*
* @param $user
* @param $event_type
* @param
*/
public static function tracks_record_event( $user, $event_type, $data ) {
$tracking = new Automattic\Jetpack\Tracking();
return $tracking->tracks_record_event( $user, $event_type, $data );
}
/**
* Determines if the current user is the site's Jetpack connection owner.
*
* @return bool Whether the current user is the Jetpack connection owner.
*/
public static function is_current_user_connection_owner() {
return self::get_connection_manager()->is_connection_owner();
}
/**
* Determines if both the blog and a blog owner account are connected to Jetpack.
*
* @return bool Whether or nor Jetpack is connected
*/
public static function is_connected() {
return self::get_connection_manager()->is_connected() &&
self::get_connection_manager()->has_connected_owner();
}
/**
* Connects the site to Jetpack.
* This code performs a redirection, so anything executed after it will be ignored.
*
* @param $redirect_url
*/
public static function connect_site( $redirect_url ) {
$connection_manager = self::get_connection_manager();
// Register the site to wp.com.
if ( ! $connection_manager->is_connected() ) {
$result = $connection_manager->try_registration();
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ), 'wc_services_jetpack_register_site_failed', 500 );
}
}
// Redirect the user to the Jetpack user connection flow.
add_filter( 'jetpack_use_iframe_authorization_flow', '__return_false' );
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- URL generated by the Jetpack Connection package.
wp_redirect(
add_query_arg(
array( 'from' => self::JETPACK_PLUGIN_SLUG ),
$connection_manager->get_authorization_url( null, $redirect_url )
)
);
exit;
}
/**
* Jetpack Connection package version.
*
* @return string
*/
public static function get_jetpack_connection_package_version() {
return Package_Version::PACKAGE_VERSION;
}
/**
* Get the WPCOM or self-hosted site ID.
*
* @return int|WP_Error
*/
public static function get_wpcom_site_id() {
return Manager::get_site_id();
}
}
}
@@ -0,0 +1,254 @@
<?php
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( ! class_exists( 'WC_Connect_Label_Reports' ) ) {
include_once WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php';
class WC_Connect_Label_Reports extends WC_Admin_Report {
const LABELS_TRANSIENT_KEY = 'wcs_label_reports';
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
public function __construct( WC_Connect_Service_Settings_Store $settings_store ) {
$this->settings_store = $settings_store;
}
public function get_export_button() {
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
?>
<a
href="#"
download="report-shipping-labels-<?php echo esc_attr( $current_range ); ?>-<?php echo esc_html( date_i18n( 'Y-m-d', current_time( 'timestamp' ) ) ); ?>.csv"
class="export_csv"
data-export="table"
>
<?php esc_html_e( 'Export CSV', 'woocommerce-services' ); ?>
</a>
<?php
}
private function compare_label_dates_desc( $label_a, $label_b ) {
return $label_b['created'] - $label_a['created'];
}
private function get_all_labels() {
global $wpdb;
$table_name = OrderUtil::get_table_for_order_meta();
$id_column = OrderUtil::custom_orders_table_usage_is_enabled() ? 'order_id' : 'post_id';
$db_results = $wpdb->get_results(
$wpdb->prepare(
'SELECT %i, meta_value FROM %i WHERE meta_key = %s',
$id_column,
$table_name,
'wc_connect_labels'
)
);
$results = array();
foreach ( $db_results as $meta ) {
$labels = maybe_unserialize( $meta->meta_value );
if ( ! is_array( $labels ) ) {
$labels = $this->settings_store->try_deserialize_labels_json( $meta->meta_value );
}
if ( empty( $labels ) ) {
continue;
}
foreach ( $labels as $label ) {
$results[] = array_merge( $label, array( 'order_id' => $meta->{$id_column} ) );
}
}
usort( $results, array( $this, 'compare_label_dates_desc' ) );
return $results;
}
private function query_labels() {
$all_labels = get_transient( self::LABELS_TRANSIENT_KEY );
if ( false === $all_labels ) {
$all_labels = $this->get_all_labels();
// set transient with ttl of 30 minutes
set_transient( self::LABELS_TRANSIENT_KEY, $all_labels, 1800 );
}
/**
* Translate timestamps to JS timestamps.
*
* The start_date is set to the beginning of the day (midnight) of the start_date property, converted to milliseconds.
* The end_date is set to the end of the day (one millisecond before midnight) of the end_date property, converted to milliseconds.
* This ensures that the date range includes the entire days specified by start_date and end_date.
*/
$start_date = strtotime( 'midnight', $this->start_date ) * 1000;
$end_date = strtotime( 'tomorrow', $this->end_date ) * 1000 - 1;
$results = array();
foreach ( $all_labels as $label ) {
$created = $label['created'];
if ( $created > $end_date ) {
continue;
}
// labels are sorted in descending order, so if we reached the end, break the loop
if ( $created < $start_date ) {
break;
}
if ( isset( $label['error'] ) || // ignore the error labels
! isset( $label['rate'] ) ) { // labels where purchase hasn't completed for any reason
continue;
}
// ignore labels with complete refunds
if ( isset( $label['refund'] ) ) {
$refund = (array) $label['refund'];
if ( isset( $refund['status'] ) && 'completed' === $refund['status'] ) {
continue;
}
}
$results[] = $label;
}
return $results;
}
public function output_report() {
$ranges = array(
'year' => __( 'Year', 'woocommerce-services' ),
'last_month' => __( 'Last month', 'woocommerce-services' ),
'month' => __( 'This month', 'woocommerce-services' ),
'7day' => __( 'Last 7 days', 'woocommerce-services' ),
);
$current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : '7day';
if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) {
$current_range = '7day';
}
$this->check_current_range_nonce( $current_range );
$this->calculate_current_range( $current_range );
$hide_sidebar = true;
include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php';
}
private function get_edit_order_link( $post_id ) {
$order = wc_get_order( $post_id );
if ( ! is_a( $order, 'WC_Order' ) ) {
return null;
}
return '<a href="' . $order->get_edit_order_url() . '">' . $order->get_order_number() . '</a>';
}
private function get_label_refund_status( $label ) {
if ( ! isset( $label['refund'] ) ) {
return '';
}
$refund = (array) $label['refund'];
if ( isset( $refund['status'] ) &&
( 'rejected' === $refund['status'] || 'complete' === $refund['status'] ) ) {
return '';
}
return __( 'Requested', 'woocommerce-services' );
}
/**
* Get the main chart.
*/
public function get_main_chart() {
$labels = $this->query_labels();
?>
<table class="widefat">
<thead>
<tr>
<th>
<?php esc_html_e( 'Time', 'woocommerce-services' ); ?>
</th>
<th>
<?php esc_html_e( 'Order', 'woocommerce-services' ); ?>
</th>
<th>
<?php esc_html_e( 'Price', 'woocommerce-services' ); ?>
</th>
<th>
<?php esc_html_e( 'Service', 'woocommerce-services' ); ?>
</th>
<th>
<?php esc_html_e( 'Refund', 'woocommerce-services' ); ?>
</th>
</tr>
</thead>
<?php if ( ! empty( $labels ) ) : ?>
<tbody>
<?php foreach ( $labels as $label ) : ?>
<tr>
<th scope="row">
<?php echo esc_html( get_date_from_gmt( date( 'Y-m-d H:i:s', intval( $label['created'] / 1000 ) ) ) ); ?>
</th>
<td>
<?php
echo wp_kses(
$this->get_edit_order_link( $label['order_id'] ),
array(
'a' => array(
'href' => array(),
),
)
);
?>
</td>
<td>
<?php echo wp_kses_post( wc_price( $label['rate'] ) ); ?>
</td>
<td>
<?php echo esc_html( $label['service_name'] ); ?>
</td>
<td>
<?php echo esc_html( $this->get_label_refund_status( $label ) ); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<?php
$total = array_sum( wp_list_pluck( $labels, 'rate' ) );
?>
<tr>
<th scope="row">
<?php esc_html_e( 'Total', 'woocommerce-services' ); ?>
</th>
<th>
<?php echo count( $labels ); ?>
</th>
<th>
<?php echo wp_kses_post( wc_price( $total ) ); ?>
</th>
<th></th>
<th></th>
</tr>
<?php else : ?>
<tbody>
<tr>
<td><?php esc_html_e( 'No labels found for this period', 'woocommerce-services' ); ?></td>
</tr>
</tbody>
<?php endif; ?>
</table>
<?php
}
}
}
@@ -0,0 +1,130 @@
<?php
if ( ! class_exists( 'WC_Connect_Logger' ) ) {
class WC_Connect_Logger {
/**
* @var WC_Logger
*/
private $logger;
private $is_logging_enabled = false;
private $is_debug_enabled = false;
private $feature;
public function __construct( WC_Logger $logger, $feature = '' ) {
$this->logger = $logger;
$this->feature = strtolower( $feature );
$this->is_logging_enabled = WC_Connect_Options::get_option( 'debug_logging_enabled', false );
$this->is_debug_enabled = WC_Connect_Options::get_option( 'debug_display_enabled', false );
}
/**
* Format a message with optional context for logging.
*
* @param string|WP_Error $message Either a string message, or WP_Error object.
* @param string $context Optional. Context for the logged message.
* @return string The formatted log message.
*/
protected function format_message( $message, $context = '' ) {
$formatted_message = $message;
if ( is_wp_error( $message ) ) {
$formatted_message = $message->get_error_code() . ' ' . $message->get_error_message();
}
if ( ! empty( $context ) ) {
$formatted_message .= ' (' . $context . ')';
}
return $formatted_message;
}
public function enable_logging() {
WC_Connect_Options::update_option( 'debug_logging_enabled', true );
$this->is_logging_enabled = true;
$this->log( 'Logging enabled' );
}
public function disable_logging() {
$this->log( 'Logging disabled' );
WC_Connect_Options::update_option( 'debug_logging_enabled', false );
$this->is_logging_enabled = false;
}
public function enable_debug() {
WC_Connect_Options::update_option( 'debug_display_enabled', true );
$this->is_debug_enabled = true;
$this->log( 'Debug enabled' );
}
public function disable_debug() {
$this->log( 'Debug disabled' );
WC_Connect_Options::update_option( 'debug_display_enabled', false );
$this->is_debug_enabled = false;
}
public function is_debug_enabled() {
return $this->is_debug_enabled;
}
public function is_logging_enabled() {
return $this->is_logging_enabled;
}
/**
* Log debug by printing it as notice when debugging is enabled.
*
* @param string $message Debug message.
* @param string $type Notice type.
*/
public function debug( $message, $type = 'notice' ) {
if ( $this->is_debug_enabled() && ! wc_has_notice( $message, $type ) ) {
wc_add_notice( $message, $type );
}
}
/**
* Logs messages even if debugging is disabled
*
* @param string $message Message to log
* @param string $context Optional context (e.g. a class or function name)
*/
public function error( $message, $context = '' ) {
WC_Connect_Error_Notice::instance()->enable_notice( $message );
$this->log( $message, $context, true );
}
/**
* Logs messages to file and error_log if WP_DEBUG
*
* @param WP_Error|string $message Message to log
* @param string $context Optional context (e.g. a class or function name)
*/
public function log( $message, $context = '', $force = false ) {
$log_message = $this->format_message( $message, $context );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( $log_message );
}
if ( ! $this->is_logging_enabled() && ! $force ) {
return;
}
$log_file = 'wc-services';
if ( ! empty( $this->feature ) ) {
$log_file .= '-' . $this->feature;
}
$this->logger->add( $log_file, $log_message );
}
}
}
@@ -0,0 +1,52 @@
<?php
/**
* WooCommerce Shipping note: DHL live rates available.
*
* Only for legacy customers that had the feature available.
*/
class WC_Connect_Note_DHL_Live_Rates_Available {
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
/**
* Name of the note for use in the database.
*/
const NOTE_NAME = 'wc-services-dhl-live-rates-available';
/**
* Maybe add note to inform WooCommerce Shipping users with legacy live rates about new DHL live rates.
*
* @param WC_Connect_Service_Schemas_Store $schemas Store schemas.
*/
public static function init( WC_Connect_Service_Schemas_Store $schemas ) {
// If store has DHL Express live rates.
$has_wc_services_dhl_express = in_array( 'wc_services_dhlexpress', $schemas->get_all_shipping_method_ids(), true );
if ( $has_wc_services_dhl_express ) {
self::possibly_add_note();
}
}
/**
* Get the note.
*
* @return Automattic\WooCommerce\Admin\Notes\Note
*/
public static function get_note() {
$note = new Automattic\WooCommerce\Admin\Notes\Note();
$note->set_title( __( 'DHL Express live rates are now available', 'woocommerce-services' ) );
$note->set_content( __( 'Add DHL Express as a shipping method to selected shipping zones to display live rates at checkout.', 'woocommerce-services' ) );
$note->set_type( Automattic\WooCommerce\Admin\Notes\Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-services' );
$note->add_action(
'go-to-shipping-zones',
__( 'Go to shipping zones', 'woocommerce-services' ),
admin_url( 'admin.php?page=wc-settings&tab=shipping' )
);
return $note;
}
}
@@ -0,0 +1,641 @@
<?php
if ( ! class_exists( 'WC_Connect_Nux' ) ) {
class WC_Connect_Nux {
/**
* Jetpack status constants.
*/
const JETPACK_NOT_CONNECTED = 'not-connected';
const JETPACK_OFFLINE_MODE = 'offline-mode';
const JETPACK_CONNECTED = 'connected';
const IS_NEW_LABEL_USER = 'wcc_is_new_label_user';
/**
* Option name for dismissing success banner
* after the JP connection flow
*/
const SHOULD_SHOW_AFTER_CXN_BANNER = 'should_display_nux_after_jp_cxn_banner';
/**
* @var WC_Connect_Tracks
*/
protected $tracks;
/**
* @var WC_Connect_Shipping_Label
*/
private $shipping_label;
function __construct( WC_Connect_Tracks $tracks, WC_Connect_Shipping_Label $shipping_label ) {
$this->tracks = $tracks;
$this->shipping_label = $shipping_label;
$this->init_pointers();
}
private function get_notice_states() {
$states = get_user_meta( get_current_user_id(), 'wc_connect_nux_notices', true );
if ( ! is_array( $states ) ) {
return array();
}
return $states;
}
public function is_notice_dismissed( $notice ) {
$notices = $this->get_notice_states();
return isset( $notices[ $notice ] ) && $notices[ $notice ];
}
public function dismiss_notice( $notice ) {
$notices = $this->get_notice_states();
$notices[ $notice ] = true;
update_user_meta( get_current_user_id(), 'wc_connect_nux_notices', $notices );
}
public function ajax_dismiss_notice() {
if ( empty( $_POST['dismissible_id'] ) ) {
return;
}
check_ajax_referer( 'wc_connect_dismiss_notice', 'nonce' );
$this->dismiss_notice( sanitize_key( $_POST['dismissible_id'] ) );
wp_die();
}
private function init_pointers() {
add_filter( 'wc_services_pointer_post.php', array( $this, 'register_order_page_labels_pointer' ) );
add_filter( 'wc_services_pointer_post.php', array( $this, 'register_new_carrier_dhl_pointer' ) );
}
public function show_pointers( $hook ) {
/*
Get admin pointers for the current admin page.
*
* @since 0.9.6
*
* @param array $pointers Array of pointers.
*/
$pointers = apply_filters( 'wc_services_pointer_' . $hook, array() );
if ( ! $pointers || ! is_array( $pointers ) ) {
return;
}
$dismissed_pointers = $this->get_dismissed_pointers();
$valid_pointers = array();
foreach ( $pointers as $pointer ) {
if ( ! in_array( $pointer['id'], $dismissed_pointers, true ) ) {
$valid_pointers[] = $pointer;
}
}
if ( empty( $valid_pointers ) ) {
return;
}
wp_enqueue_style( 'wp-pointer' );
wp_localize_script( 'wc_services_admin_pointers', 'wcServicesAdminPointers', $valid_pointers );
wp_enqueue_script( 'wc_services_admin_pointers' );
}
public function get_dismissed_pointers() {
$data = get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true );
if ( is_string( $data ) && 0 < strlen( $data ) ) {
return explode( ',', $data );
}
return array();
}
/**
* Dismiss a WP pointer for the current user.
*
* @param string $pointer_to_dismiss Pointer ID to dismiss for the current user
*/
public function dismiss_pointer( $pointer_to_dismiss ) {
$dismissed_pointers = $this->get_dismissed_pointers();
if ( in_array( $pointer_to_dismiss, $dismissed_pointers, true ) ) {
return;
}
$dismissed_pointers[] = $pointer_to_dismiss;
$dismissed_data = implode( ',', $dismissed_pointers );
update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed_data );
}
public function is_new_labels_user() {
$is_new_user = get_transient( self::IS_NEW_LABEL_USER );
if ( false === $is_new_user ) {
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT 1",
'wc_connect_labels'
)
);
$is_new_user = 0 === count( $results ) ? 'yes' : 'no';
set_transient( self::IS_NEW_LABEL_USER, $is_new_user );
}
return 'yes' === $is_new_user;
}
public function register_order_page_labels_pointer( $pointers ) {
// If the user is not new to labels, we should just dismiss this pointer
if ( ! $this->is_new_labels_user() ) {
$this->dismiss_pointer( 'wc_services_labels_metabox' );
return $pointers;
}
global $post;
if ( ! $this->shipping_label->should_show_meta_box( $post ) ) {
return $pointers;
}
$supported_carriers = array( 'USPS' );
if ( $this->shipping_label->is_dhl_express_available() ) {
$supported_carriers[] = 'DHL';
}
$pointers[] = array(
'id' => 'wc_services_labels_metabox',
'target' => '#woocommerce-order-label .button',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p>%s</p>',
__( 'Discounted Shipping Labels', 'woocommerce-services' ),
sprintf( __( "When you're ready, purchase and print discounted labels from %s right here.", 'woocommerce-services' ), implode( ' or ', $supported_carriers ) )
),
'position' => array(
'edge' => 'top',
'align' => 'left',
),
),
'dim' => true,
);
return $pointers;
}
public function register_new_carrier_dhl_pointer( $pointers ) {
// new user? no need to show this alert, `wc_services_labels_metabox` will take care of communicating about DHL
if ( $this->is_new_labels_user() ) {
$this->dismiss_pointer( 'wc_services_new_carrier_dhl_express' );
return $pointers;
}
// existing user? figure out if the order supports DHL, then let them know DHL is a new carrier!
if ( ! $this->shipping_label->is_order_dhl_express_eligible() ) {
return $pointers;
}
$pointers[] = array(
'id' => 'wc_services_new_carrier_dhl_express',
'target' => '#woocommerce-order-label .button',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p>%s</p>',
__( 'Discounted DHL Shipping Labels', 'woocommerce-services' ),
__( 'WooCommerce Shipping now supports DHL labels for international shipments. Purchase and print discounted labels from DHL and USPS right here.', 'woocommerce-services' )
),
'position' => array(
'edge' => 'top',
'align' => 'left',
),
),
'dim' => true,
);
return $pointers;
}
public static function get_banner_type_to_display( $status = array() ) {
if ( ! isset( $status['jetpack_connection_status'] ) ) {
return false;
}
/*
The NUX Flow:
- Case 1: Jetpack not connected (with TOS or no TOS accepted):
1. show_banner_before_connection()
2. connect to JP
3. show_banner_after_connection(), which sets the TOS acceptance in options
- Case 2: Jetpack connected, no TOS
1. show_tos_only_banner(), which accepts TOS on button click
- Case 3: Jetpack connected, and TOS accepted
This is an existing user. Do nothing.
*/
switch ( $status['jetpack_connection_status'] ) {
case self::JETPACK_NOT_CONNECTED:
return 'before_jetpack_connection';
case self::JETPACK_CONNECTED:
case self::JETPACK_OFFLINE_MODE:
// Has the user just gone through our NUX connection flow?
if ( isset( $status['should_display_after_cxn_banner'] ) && $status['should_display_after_cxn_banner'] ) {
return 'after_jetpack_connection';
}
// Has the user already accepted our TOS? Then do nothing.
// Note: TOS is accepted during the after_connection banner
if (
isset( $status['tos_accepted'] )
&& ! $status['tos_accepted']
&& isset( $status['can_accept_tos'] )
&& $status['can_accept_tos']
) {
return 'tos_only_banner';
}
return false;
default:
return false;
}
}
public function get_jetpack_install_status() {
if ( WC_Connect_Jetpack::is_offline_mode() ) {
// activated, and dev mode on
return self::JETPACK_OFFLINE_MODE;
}
// dev mode off, check if connected
if ( ! WC_Connect_Jetpack::is_connected() ) {
return self::JETPACK_NOT_CONNECTED;
}
return self::JETPACK_CONNECTED;
}
public function should_display_nux_notice_on_screen( $screen ) {
if ( // Display if on any of these admin pages.
( // Products list.
'product' === $screen->post_type
&& 'edit' === $screen->base
)
|| ( // Orders list and edit order page when not using HPOS.
'shop_order' === $screen->post_type
&& in_array( $screen->base, array( 'edit', 'post' ), true )
)
|| ( // Orders list and edit order page when using HPOS.
wc_get_page_screen_id( 'shop_order' ) === $screen->id
)
|| ( // WooCommerce settings.
'woocommerce_page_wc-settings' === $screen->base
)
|| ( // WooCommerce featured extension page
'woocommerce_page_wc-addons' === $screen->base
&& isset( $_GET['section'] ) && 'featured' === $_GET['section']
)
|| ( // WooCommerce shipping extension page
'woocommerce_page_wc-addons' === $screen->base
&& isset( $_GET['section'] ) && 'shipping_methods' === $_GET['section']
)
|| 'plugins' === $screen->base
) {
return true;
}
return false;
}
/**
* https://developers.taxjar.com/api/reference/#countries
*/
public function is_taxjar_supported_country( $country_code ) {
$taxjar_supported_countries = array_merge(
array(
'US',
'CA',
'AU',
),
WC()->countries->get_european_union_countries()
);
return in_array( $country_code, $taxjar_supported_countries );
}
public function should_display_nux_notice_for_current_store_locale() {
$store_country = WC()->countries->get_base_country();
$supports_taxes = $this->is_taxjar_supported_country( $store_country );
$supports_shipping = in_array( $store_country, array( 'US', 'CA' ) );
return $supports_shipping || $supports_taxes;
}
public function get_feature_list_for_country( $country ) {
$feature_list = false;
$supports_taxes = $this->is_taxjar_supported_country( $country );
$supports_labels = ( 'US' === $country );
$is_ppec_active = is_plugin_active( 'woocommerce-gateway-paypal-express-checkout/woocommerce-gateway-paypal-express-checkout.php' );
$ppec_settings = get_option( 'woocommerce_ppec_paypal_settings', array() );
$supports_payments = $is_ppec_active && ( ! isset( $ppec_settings['enabled'] ) || 'yes' === $ppec_settings['enabled'] );
if ( $supports_payments && $supports_taxes && $supports_labels ) {
$feature_list = __( 'automated tax calculation, shipping label printing, and smoother payment setup', 'woocommerce-services' );
} elseif ( $supports_payments && $supports_taxes ) {
$feature_list = __( 'automated tax calculation and smoother payment setup', 'woocommerce-services' );
} elseif ( $supports_taxes && $supports_labels ) {
$feature_list = __( 'automated tax calculation and shipping label printing', 'woocommerce-services' );
} elseif ( $supports_payments && $supports_labels ) {
$feature_list = __( 'shipping label printing and smoother payment setup', 'woocommerce-services' );
} elseif ( $supports_payments ) {
$feature_list = __( 'smoother payment setup', 'woocommerce-services' );
} elseif ( $supports_taxes ) {
$feature_list = __( 'automated tax calculation', 'woocommerce-services' );
} elseif ( $supports_labels ) {
$feature_list = __( 'shipping label printing', 'woocommerce-services' );
}
return $feature_list;
}
public function get_jetpack_redirect_url() {
$full_path = add_query_arg( array() );
// Remove [...]/wp-admin so we can use admin_url().
$new_index = strpos( $full_path, '/wp-admin' ) + strlen( '/wp-admin' );
$path = substr( $full_path, $new_index );
return esc_url( admin_url( $path ) );
}
public function set_up_nux_notices() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
// Check for plugin install and activate permissions to handle Jetpack on multisites:
// Admins might not be able to install or activate plugins, but Jetpack might already have been installed by a superadmin.
// If this is the case, the admin can connect the site on their own, and should be able to use WCS as ususal
$jetpack_install_status = $this->get_jetpack_install_status();
$banner_to_display = self::get_banner_type_to_display(
array(
'jetpack_connection_status' => $jetpack_install_status,
'tos_accepted' => WC_Connect_Options::get_option( 'tos_accepted' ),
'can_accept_tos' => WC_Connect_Jetpack::is_current_user_connection_owner() || WC_Connect_Jetpack::is_offline_mode(),
'should_display_after_cxn_banner' => WC_Connect_Options::get_option( self::SHOULD_SHOW_AFTER_CXN_BANNER ),
)
);
switch ( $banner_to_display ) {
case 'before_jetpack_connection':
wp_enqueue_script( 'wc_connect_banner' );
add_action(
'admin_post_register_woocommerce_services_jetpack',
array( $this, 'register_woocommerce_services_jetpack' )
);
wp_enqueue_style( 'wc_connect_banner' );
add_action( 'admin_notices', array( $this, 'show_banner_before_connection' ), 9 );
break;
case 'tos_only_banner':
wp_enqueue_style( 'wc_connect_banner' );
add_action( 'admin_notices', array( $this, 'show_tos_banner' ) );
break;
case 'after_jetpack_connection':
wp_enqueue_style( 'wc_connect_banner' );
add_action( 'admin_notices', array( $this, 'show_banner_after_connection' ) );
break;
}
add_action( 'wp_ajax_wc_connect_dismiss_notice', array( $this, 'ajax_dismiss_notice' ) );
}
public function show_banner_before_connection() {
if ( ! $this->should_display_nux_notice_for_current_store_locale() ) {
return;
}
if ( ! $this->should_display_nux_notice_on_screen( get_current_screen() ) ) {
return;
}
// Remove Jetpack's connect banners since we're showing our own.
if ( class_exists( 'Jetpack_Connection_Banner' ) ) {
$jetpack_banner = Jetpack_Connection_Banner::init();
remove_action( 'admin_notices', array( $jetpack_banner, 'render_banner' ) );
remove_action( 'admin_notices', array( $jetpack_banner, 'render_connect_prompt_full_screen' ) );
}
// Make sure that we wait until the button is clicked before displaying
// the after_connection banner
// so that we don't accept the TOS pre-maturely
WC_Connect_Options::delete_option( self::SHOULD_SHOW_AFTER_CXN_BANNER );
$country = WC()->countries->get_base_country();
/* translators: %s: list of features, potentially comma separated */
$description_base = __( "WooCommerce Shipping & Tax is almost ready to go! Once you connect your site to WordPress.com you'll have access to %s.", 'woocommerce-services' );
$feature_list = $this->get_feature_list_for_country( $country );
$banner_content = array(
'title' => __( 'Connect your site to activate WooCommerce Shipping & Tax', 'woocommerce-services' ),
'description' => sprintf( $description_base, $feature_list ),
'button_text' => __( 'Connect', 'woocommerce-services' ),
'image_url' => plugins_url( 'images/wcs-notice.png', dirname( __FILE__ ) ),
'should_show_terms' => true,
);
$this->show_nux_banner( $banner_content );
}
public function show_banner_after_connection() {
if ( ! $this->should_display_nux_notice_for_current_store_locale() ) {
return;
}
if ( ! $this->should_display_nux_notice_on_screen( get_current_screen() ) ) {
return;
}
// Did the user just dismiss?
if ( isset( $_GET['wcs-nux-notice'] ) && 'dismiss' === $_GET['wcs-nux-notice'] ) {
// No longer need to keep track of whether the before connection banner was displayed.
WC_Connect_Options::delete_option( self::SHOULD_SHOW_AFTER_CXN_BANNER );
wp_safe_redirect( remove_query_arg( 'wcs-nux-notice' ) );
exit;
}
// By going through the connection process, the user has accepted our TOS
WC_Connect_Options::update_option( 'tos_accepted', true );
$this->tracks->opted_in( 'connection_banner' );
$country = WC()->countries->get_base_country();
/* translators: %s: list of features, potentially comma separated */
$description_base = __( 'You can now enjoy %s.', 'woocommerce-services' );
$feature_list = $this->get_feature_list_for_country( $country );
$this->show_nux_banner(
array(
'title' => __( 'Setup complete.', 'woocommerce-services' ),
'description' => esc_html( sprintf( $description_base, $feature_list ) ),
'button_text' => __( 'Got it, thanks!', 'woocommerce-services' ),
'button_link' => add_query_arg(
array(
'wcs-nux-notice' => 'dismiss',
)
),
'image_url' => plugins_url(
'images/wcs-notice.png',
dirname( __FILE__ )
),
'should_show_terms' => false,
)
);
}
public function show_tos_banner() {
if ( ! $this->should_display_nux_notice_for_current_store_locale() ) {
return;
}
if ( ! $this->should_display_nux_notice_on_screen( get_current_screen() ) ) {
return;
}
if ( isset( $_GET['wcs-nux-tos'] ) && 'accept' === $_GET['wcs-nux-tos'] ) {
WC_Connect_Options::update_option( 'tos_accepted', true );
$this->tracks->opted_in( 'tos_banner' );
wp_safe_redirect( remove_query_arg( 'wcs-nux-tos' ) );
exit;
}
$country = WC()->countries->get_base_country();
/* translators: %s: list of features, potentially comma separated */
$description_base = __( "WooCommerce Shipping & Tax is almost ready to go! Once you connect your site to WordPress.com you'll have access to %s.", 'woocommerce-services' );
$feature_list = $this->get_feature_list_for_country( $country );
$this->show_nux_banner(
array(
'title' => __( 'Connect your site to activate WooCommerce Shipping & Tax', 'woocommerce-services' ),
'description' => esc_html( sprintf( $description_base, $feature_list ) ),
'button_text' => __( 'Connect', 'woocommerce-services' ),
'button_link' => add_query_arg(
array(
'wcs-nux-tos' => 'accept',
)
),
'image_url' => plugins_url(
'images/wcs-notice.png',
dirname( __FILE__ )
),
'should_show_terms' => true,
)
);
}
public function show_nux_banner( $content ) {
if ( isset( $content['dismissible_id'] ) && $this->is_notice_dismissed( sanitize_key( $content['dismissible_id'] ) ) ) {
return;
}
?>
<div class="notice wcs-nux__notice <?php echo isset( $content['dismissible_id'] ) ? 'is-dismissible' : ''; ?>">
<div class="wcs-nux__notice-logo <?php echo isset( $content['compact_logo'] ) && $content['compact_logo'] ? 'is-compact' : ''; ?>">
<img class="wcs-nux__notice-logo-graphic" src="<?php echo esc_url( $content['image_url'] ); ?>">
</div>
<div class="wcs-nux__notice-content">
<h1 class="wcs-nux__notice-content-title">
<?php echo esc_html( $content['title'] ); ?>
</h1>
<p class="wcs-nux__notice-content-text">
<?php echo esc_html( $content['description'] ); ?>
</p>
<?php if ( isset( $content['should_show_terms'] ) && $content['should_show_terms'] ) : ?>
<p class="wcs-nux__notice-content-tos">
<?php
/* translators: %1$s example values include "Install Jetpack and CONNECT >", "Activate Jetpack and CONNECT >", "CONNECT >" */
printf(
wp_kses(
__( 'By clicking "%1$s", you agree to our <a href="%2$s">Terms of Service</a> and have read our <a href="%3$s">Privacy Policy</a>.', 'woocommerce-services' ),
array(
'a' => array(
'href' => array(),
),
)
),
esc_html( $content['button_text'] ),
'https://wordpress.com/tos/',
'https://automattic.com/privacy/'
);
?>
</p>
<?php endif; ?>
<?php if ( isset( $content['button_link'] ) ) : ?>
<a
class="wcs-nux__notice-content-button button button-primary"
href="<?php echo esc_url( $content['button_link'] ); ?>"
>
<?php echo esc_html( $content['button_text'] ); ?>
</a>
<?php else : ?>
<form action="<?php echo esc_attr( admin_url( 'admin-post.php' ) ); ?>" method="post">
<input type="hidden" name="action" value="register_woocommerce_services_jetpack"/>
<input type="hidden" name="redirect_url"
value="<?php echo esc_url( $this->get_jetpack_redirect_url() ); ?>"/>
<?php wp_nonce_field( 'wcs_nux_notice' ); ?>
<button
type="submit"
class="woocommerce-services__connect-jetpack wcs-nux__notice-content-button button button-primary"
>
<?php echo esc_html( $content['button_text'] ); ?>
</button>
</form>
<?php endif; ?>
</div>
</div>
<?php
if ( isset( $content['dismissible_id'] ) ) :
// Add handler for dismissing banner. Only supports a single banner at a time
wp_enqueue_script( 'wp-util' );
?>
<script>
(
function ($) {
$('.wcs-nux__notice').on('click', '.notice-dismiss', function () {
wp.ajax.post({
action: 'wc_connect_dismiss_notice',
dismissible_id: "<?php echo esc_js( $content['dismissible_id'] ); ?>",
nonce: "<?php echo esc_js( wp_create_nonce( 'wc_connect_dismiss_notice' ) ); ?>"
})
})
}
)(jQuery)
</script>
<?php
endif;
}
/**
* Connects the site to Jetpack.
*/
public function register_woocommerce_services_jetpack() {
check_admin_referer( 'wcs_nux_notice' );
$redirect_url = '';
if ( isset( $_POST['redirect_url'] ) ) {
$redirect_url = esc_url_raw( wp_unslash( $_POST['redirect_url'] ) );
}
// Make sure we always display the after-connection banner
// after the before_connection button is clicked
WC_Connect_Options::update_option( self::SHOULD_SHOW_AFTER_CXN_BANNER, true );
WC_Connect_Jetpack::connect_site( $redirect_url );
}
}
}
@@ -0,0 +1,340 @@
<?php
if ( ! class_exists( 'WC_Connect_Options' ) ) {
class WC_Connect_Options {
/**
* An array that maps a grouped option type to an option name.
*
* @var array
*/
private static $grouped_options = array(
'compact' => 'wc_connect_options',
);
/**
* Returns an array of option names for a given type.
*
* @param string $type The type of option to return. Defaults to 'compact'.
*
* @return array
*/
public static function get_option_names( $type = 'compact' ) {
switch ( $type ) {
case 'non_compact':
return array(
'error_notice',
'services',
'services_last_update',
'last_heartbeat',
'origin_address',
'last_rate_request',
'services_last_result_code',
);
case 'shipping_method':
return array(
'form_settings',
'failure_timestamp',
);
}
return array(
'tos_accepted',
'store_guid',
'debug_logging_enabled',
'debug_display_enabled',
'add_payment_method_url',
'payment_methods',
'account_settings',
'paper_size',
'packages',
'predefined_packages',
'shipping_methods_migrated',
'should_display_nux_after_jp_cxn_banner',
'needs_tax_environment_setup',
'banner_ppec',
);
}
/**
* Deletes all options created by WooCommerce Shipping & Tax, including shipping method options
*/
public static function delete_all_options() {
if ( defined( 'WOOCOMMERCE_CONNECT_DEV_SERVER_URL' ) ) {
return;
}
foreach ( self::$grouped_options as $group_key => $group ) {
// delete legacy options
foreach ( self::get_option_names( $group_key ) as $group_option ) {
delete_option( "wc_connect_$group_option" );
}
delete_option( $group );
}
$non_compacts = self::get_option_names( 'non_compact' );
foreach ( $non_compacts as $non_compact ) {
delete_option( "wc_connect_$non_compact" );
}
self::delete_all_shipping_methods_options();
}
/**
* Returns the requested option. Looks in wc_connect_options or wc_connect_$name as appropriate.
*
* @param string $name Option name
* @param mixed $default (optional)
*
* @return mixed
*/
public static function get_option( $name, $default = false ) {
if ( self::is_valid( $name, 'non_compact' ) ) {
return get_option( "wc_connect_$name", $default );
}
foreach ( array_keys( self::$grouped_options ) as $group ) {
if ( self::is_valid( $name, $group ) ) {
return self::get_grouped_option( $group, $name, $default );
}
}
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax option name: %s', $name ), E_USER_WARNING ) );
return $default;
}
/**
* Updates the single given option. Updates wc_connect_options or wc_connect_$name as appropriate.
*
* @param string $name Option name
* @param mixed $value Option value
*
* @return bool Was the option successfully updated?
*/
public static function update_option( $name, $value ) {
if ( self::is_valid( $name, 'non_compact' ) ) {
return update_option( "wc_connect_$name", $value );
}
foreach ( array_keys( self::$grouped_options ) as $group ) {
if ( self::is_valid( $name, $group ) ) {
return self::update_grouped_option( $group, $name, $value );
}
}
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax option name: %s', $name ), E_USER_WARNING ) );
return false;
}
/**
* Deletes the given option. May be passed multiple option names as an array.
* Updates wc_connect_options and/or deletes wc_connect_$name as appropriate.
*
* @param string|array $names
*
* @return bool Was the option successfully deleted?
*/
public static function delete_option( $names ) {
$result = true;
$names = (array) $names;
if ( ! self::is_valid( $names ) ) {
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax option names: %s', print_r( $names, 1 ) ), E_USER_WARNING ) );
return false;
}
foreach ( array_intersect( $names, self::get_option_names( 'non_compact' ) ) as $name ) {
if ( ! delete_option( "wc_connect_$name" ) ) {
$result = false;
}
}
foreach ( array_keys( self::$grouped_options ) as $group ) {
if ( ! self::delete_grouped_option( $group, $names ) ) {
$result = false;
}
}
return $result;
}
/**
* Gets a shipping method option
*
* @param $name
* @param $default
* @param $service_id
* @param $service_instance
*
* @return mixed
*/
public static function get_shipping_method_option( $name, $default, $service_id, $service_instance = false ) {
$option_name = self::get_shipping_method_option_name( $name, $service_id, $service_instance );
if ( ! $option_name ) {
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax shipping method option name: %s', $name ), E_USER_WARNING ) );
return $default;
}
return get_option( $option_name, $default );
}
/**
* Updates a shipping method option
*
* @param $name
* @param $value
* @param $service_id
* @param $service_instance
*
* @return bool
*/
public static function update_shipping_method_option( $name, $value, $service_id, $service_instance = false ) {
$option_name = self::get_shipping_method_option_name( $name, $service_id, $service_instance );
if ( ! $option_name ) {
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax shipping method option name: %s', $name ), E_USER_WARNING ) );
return false;
}
return update_option( $option_name, $value );
}
/**
* Deletes a shipping method option
*
* @param $name
* @param $service_id
* @param $service_instance
*
* @return bool
*/
public static function delete_shipping_method_option( $name, $service_id, $service_instance = false ) {
$option_name = self::get_shipping_method_option_name( $name, $service_id, $service_instance );
if ( ! $option_name ) {
trigger_error( esc_html( sprintf( 'Invalid WooCommerce Shipping & Tax shipping method option name: %s', $name ), E_USER_WARNING ) );
return false;
}
return delete_option( $option_name );
}
/**
* Deletes all options related to a shipping method
*
* @param $service_id
* @param $service_instance
*/
public static function delete_shipping_method_options( $service_id, $service_instance = false ) {
$option_names = self::get_option_names( 'shipping_method' );
foreach ( $option_names as $name ) {
delete_option( self::get_shipping_method_option_name( $name, $service_id, $service_instance ) );
}
}
private static function get_grouped_option( $group, $name, $default ) {
$options = get_option( self::$grouped_options[ $group ] );
if ( is_array( $options ) && isset( $options[ $name ] ) ) {
return $options[ $name ];
}
// make the grouped options backwards-compatible and migrate the old options
$legacy_name = "wc_connect_$name";
$legacy_option = get_option( $legacy_name, false );
if ( ! $legacy_option ) {
return $default;
}
if ( self::update_grouped_option( $group, $name, $legacy_option ) ) {
delete_option( $legacy_name );
}
return $legacy_option;
}
private static function update_grouped_option( $group, $name, $value ) {
$options = get_option( self::$grouped_options[ $group ] );
if ( ! is_array( $options ) ) {
$options = array();
}
$options[ $name ] = $value;
return update_option( self::$grouped_options[ $group ], $options );
}
private static function delete_grouped_option( $group, $names ) {
$options = get_option( self::$grouped_options[ $group ], array() );
$to_delete = array_intersect( $names, self::get_option_names( $group ), array_keys( $options ) );
if ( $to_delete ) {
foreach ( $to_delete as $name ) {
unset( $options[ $name ] );
}
return update_option( self::$grouped_options[ $group ], $options );
}
return true;
}
/**
* Based on the service id and optional instance, generates the option name
*
* @param $name
* @param $service_id
* @param $service_instance
*
* @return string|bool
*/
private static function get_shipping_method_option_name( $name, $service_id, $service_instance = false ) {
if ( ! in_array( $name, self::get_option_names( 'shipping_method' ) ) ) {
return false;
}
if ( ! $service_instance ) {
return 'woocommerce_' . $service_id . '_' . $name;
}
return 'woocommerce_' . $service_id . '_' . $service_instance . '_' . $name;
}
/**
* Is the option name valid?
*
* @param string $name The name of the option
* @param string $group The name of the group that the option is in. Defaults to compact.
*
* @return bool Is the option name valid?
*/
private static function is_valid( $name, $group = 'non_compact' ) {
$group_keys = array_keys( self::$grouped_options );
if ( is_array( $name ) ) {
$compact_names = array();
foreach ( $group_keys as $_group ) {
$compact_names = array_merge( $compact_names, self::get_option_names( $_group ) );
}
$result = array_diff( $name, self::get_option_names( 'non_compact' ), $compact_names );
return empty( $result );
}
if ( is_null( $group ) || 'non_compact' === $group ) {
if ( in_array( $name, self::get_option_names( $group ) ) ) {
return true;
}
}
foreach ( array_keys( self::$grouped_options ) as $_group ) {
if ( is_null( $group ) || $group === $_group ) {
if ( in_array( $name, self::get_option_names( $_group ) ) ) {
return true;
}
}
}
return false;
}
/**
* Deletes all options of all shipping methods
*/
private static function delete_all_shipping_methods_options() {
global $wpdb;
$methods = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_shipping_zone_methods " );
foreach ( (array) $methods as $method ) {
self::delete_shipping_method_options( $method->method_id, $method->instance_id );
}
}
}
}
@@ -0,0 +1,163 @@
<?php
if ( ! class_exists( 'WC_Connect_Order_Presenter' ) ) {
class WC_Connect_Order_Presenter {
/**
* This function transform the WC_Order object to a representational JSON form for the react app.
* This is based on WooCommerce v3's get_order API woocommerce/includes/legacy/api/v3/class-wc-api-orders.php
*
* @param WC_Order $order
* @return array
*/
public function get_order_for_api( WC_Order $order ) {
$decimal_point = 2;
$order_data = array(
'id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'order_key' => $order->get_order_key(),
'created_at' => $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0,
'updated_at' => wc_format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0 ),
'completed_at' => wc_format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0 ),
'status' => $order->get_status(),
'currency' => $order->get_currency(),
'total' => wc_format_decimal( $order->get_total(), $decimal_point ),
'subtotal' => wc_format_decimal( $order->get_subtotal(), $decimal_point ),
'total_line_items_quantity' => $order->get_item_count(),
'total_tax' => wc_format_decimal( $order->get_total_tax(), $decimal_point ),
'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $decimal_point ),
'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $decimal_point ),
'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $decimal_point ),
'total_discount' => wc_format_decimal( $order->get_total_discount(), $decimal_point ),
'shipping_methods' => $order->get_shipping_method(),
'payment_details' => array(
'method_id' => $order->get_payment_method(),
'method_title' => $order->get_payment_method_title(),
'paid' => ! is_null( $order->get_date_paid() ),
),
'billing_address' => array(
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'company' => $order->get_billing_company(),
'address_1' => $order->get_billing_address_1(),
'address_2' => $order->get_billing_address_2(),
'city' => $order->get_billing_city(),
'state' => $order->get_billing_state(),
'postcode' => $order->get_billing_postcode(),
'country' => $order->get_billing_country(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
),
'shipping_address' => array(
'first_name' => $order->get_shipping_first_name(),
'last_name' => $order->get_shipping_last_name(),
'company' => $order->get_shipping_company(),
'address_1' => $order->get_shipping_address_1(),
'address_2' => $order->get_shipping_address_2(),
'city' => $order->get_shipping_city(),
'state' => $order->get_shipping_state(),
'postcode' => $order->get_shipping_postcode(),
'country' => $order->get_shipping_country(),
),
'note' => $order->get_customer_note(),
'customer_ip' => $order->get_customer_ip_address(),
'customer_user_agent' => $order->get_customer_user_agent(),
'customer_id' => $order->get_user_id(),
'view_order_url' => $order->get_view_order_url(),
'line_items' => array(),
'shipping_lines' => array(),
'tax_lines' => array(),
'fee_lines' => array(),
'coupon_lines' => array(),
);
// Add line items.
/**
* WC Order Item Product.
*
* @var WC_Order_Item_Product $item
*/
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$item_meta = $item->get_formatted_meta_data();
foreach ( $item_meta as $key => $values ) {
$item_meta[ $key ]->label = $values->display_key;
unset( $item_meta[ $key ]->display_key );
unset( $item_meta[ $key ]->display_value );
}
$line_item = array(
'id' => $item_id,
'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $decimal_point ),
'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $decimal_point ),
'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $decimal_point ),
'total_tax' => wc_format_decimal( $item->get_total_tax(), $decimal_point ),
'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $decimal_point ),
'quantity' => $item->get_quantity(),
'tax_class' => $item->get_tax_class(),
'name' => $item->get_name(),
'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(),
'sku' => is_object( $product ) ? $product->get_sku() : null,
'meta' => array_values( $item_meta ),
);
$order_data['line_items'][] = $line_item;
}
// Add shipping.
foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
$order_data['shipping_lines'][] = array(
'id' => $shipping_item_id,
'method_id' => $shipping_item->get_method_id(),
'method_title' => $shipping_item->get_name(),
'total' => wc_format_decimal( $shipping_item->get_total(), $decimal_point ),
);
}
// Add taxes.
foreach ( $order->get_tax_totals() as $tax_code => $tax ) {
$tax_line = array(
'id' => $tax->id,
'rate_id' => $tax->rate_id,
'code' => $tax_code,
'title' => $tax->label,
'total' => wc_format_decimal( $tax->amount, $decimal_point ),
'compound' => (bool) $tax->is_compound,
);
$order_data['tax_lines'][] = $tax_line;
}
// Add fees.
foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
$order_data['fee_lines'][] = array(
'id' => $fee_item_id,
'title' => $fee_item->get_name(),
'tax_class' => $fee_item->get_tax_class(),
'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $decimal_point ),
'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $decimal_point ),
);
}
// Add coupons.
/**
* WC Order Item Coupon.
*
* @var WC_Order_Item_Coupon $coupon_item
*/
foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
$coupon_line = array(
'id' => $coupon_item_id,
'code' => $coupon_item->get_code(),
'amount' => wc_format_decimal( $coupon_item->get_discount(), $decimal_point ),
);
$order_data['coupon_lines'][] = $coupon_line;
}
return $order_data;
}
}
}
@@ -0,0 +1,38 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WC_Connect_Package_Settings {
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
public function __construct(
WC_Connect_Service_Settings_Store $settings_store,
WC_Connect_Service_Schemas_Store $service_schemas_store
) {
$this->settings_store = $settings_store;
$this->service_schemas_store = $service_schemas_store;
}
public function get() {
return array(
'storeOptions' => $this->settings_store->get_store_options(),
'formSchema' => array(
'custom' => $this->service_schemas_store->get_packages_schema(),
'predefined' => $this->service_schemas_store->get_predefined_packages_schema(),
),
'formData' => array(
'custom' => $this->settings_store->get_packages(),
'predefined' => $this->settings_store->get_predefined_packages(),
),
);
}
}
@@ -0,0 +1,20 @@
<?php
if ( ! class_exists( 'WC_Connect_Payment_Gateway' ) ) {
class WC_Connect_Payment_Gateway extends WC_Payment_Gateway {
public function __construct( $settings ) {
foreach ( (array) $settings as $key => $value ) {
$this->{$key} = $value;
}
$this->init_settings();
}
}
}
@@ -0,0 +1,167 @@
<?php
if ( ! class_exists( 'WC_Connect_Payment_Methods_Store' ) ) {
class WC_Connect_Payment_Methods_Store {
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $service_settings_store;
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* @var WC_Connect_Logger
*/
protected $logger;
public function __construct( WC_Connect_Service_Settings_Store $service_settings_store,
WC_Connect_API_Client $api_client, WC_Connect_Logger $logger ) {
$this->service_settings_store = $service_settings_store;
$this->api_client = $api_client;
$this->logger = $logger;
}
/**
* Fetch stored payment methods from server and store in options.
*
* @return bool Were payment methods successfully retrieved?
*/
public function fetch_payment_methods_from_connect_server() {
$response_body = $this->api_client->get_payment_methods();
if ( is_wp_error( $response_body ) ) {
$this->logger->log( $response_body, __FUNCTION__ );
return false;
}
$validation = $this->validate_payment_methods_response( $response_body );
if ( is_wp_error( $validation ) ) {
$this->logger->log( sprintf( '[%s] %s', $validation->get_error_code(), $validation->get_error_message() ), __FUNCTION__ );
return false;
}
// Get add payment method url from response body.
$add_payment_method_url = $this->get_add_payment_method_url_from_response_body( $response_body );
$payment_methods = $this->get_payment_methods_from_response_body( $response_body );
// Store the payment methods and add payment method url.
$this->update_add_payment_method_url( $add_payment_method_url );
$this->update_payment_methods( $payment_methods );
$this->potentially_update_selected_payment_method_from_payment_methods( $payment_methods );
return true;
}
protected function potentially_update_selected_payment_method_from_payment_methods( $payment_methods ) {
$payment_method_ids = array();
foreach ( (array) $payment_methods as $payment_method ) {
$payment_method_id = intval( $payment_method->payment_method_id );
if ( 0 !== $payment_method_id ) {
$payment_method_ids[] = $payment_method_id;
}
}
// No payment methods at all? Clear anything we have stored.
if ( 0 === count( $payment_method_ids ) ) {
$this->service_settings_store->set_selected_payment_method_id( 0 );
return;
}
// Has the stored method ID been removed, or is there only one available? Select the first available one.
$selected_payment_method_id = $this->service_settings_store->get_selected_payment_method_id();
if (
( $selected_payment_method_id || 1 === count( $payment_method_ids ) ) &&
! in_array( $selected_payment_method_id, $payment_method_ids )
) {
$this->service_settings_store->set_selected_payment_method_id( $payment_method_ids[0] );
}
}
public function get_payment_methods() {
return WC_Connect_Options::get_option( 'payment_methods', array() );
}
protected function update_payment_methods( $payment_methods ) {
WC_Connect_Options::update_option( 'payment_methods', $payment_methods );
}
/**
* Validate that the response body is valid and contains the correct properties.
*
* @param object $response_body The response body object.
* @return true|WP_Error Whether the response body is valid.
*/
protected function validate_payment_methods_response( $response_body ) {
if ( ! is_object( $response_body ) ) {
return new WP_Error( 'payment_method_response_body_type', __( 'Expected but did not receive object for response body.', 'woocommerce-services' ) );
}
if ( ! property_exists( $response_body, 'payment_methods' ) ) {
return new WP_Error( 'payment_method_response_body_missing_payment_methods', __( 'Expected but did not receive payment_methods in response body.', 'woocommerce-services' ) );
}
if ( ! property_exists( $response_body, 'add_payment_method_url' ) ) {
return new WP_Error( 'payment_method_response_body_missing_add_payment_method_url', __( 'Expected but did not receive add_payment_method_url in response body.', 'woocommerce-services' ) );
}
return true;
}
protected function get_payment_methods_from_response_body( $response_body ) {
$payment_methods = $response_body->payment_methods;
if ( ! is_array( $payment_methods ) ) {
return new WP_Error( 'payment_methods_type', 'Expected but did not receive array for payment_methods.' );
}
foreach ( $payment_methods as $payment_method ) {
$required_keys = array( 'payment_method_id', 'name', 'card_type', 'card_digits', 'expiry' );
foreach ( $required_keys as $required_key ) {
if ( ! property_exists( $payment_method, $required_key ) ) {
return new WP_Error( 'payment_methods_key_missing', 'Payment method is missing a required property' );
}
}
}
return $payment_methods;
}
/**
* Get the URL to add a payment method from the response body.
*
* @param object $response_body The response body object.
* @return string The URL to add a payment method.
*/
protected function get_add_payment_method_url_from_response_body( $response_body ) {
return $response_body->add_payment_method_url;
}
/**
* Get the URL to add a payment method.
*
* @return string The URL to add a payment method.
*/
public function get_add_payment_method_url() {
return WC_Connect_Options::get_option( 'add_payment_method_url', '' );
}
/**
* Update the URL to add a payment method.
*
* @param string $add_payment_method_url The URL to add a payment method.
* @return void
*/
protected function update_add_payment_method_url( $add_payment_method_url ) {
WC_Connect_Options::update_option( 'add_payment_method_url', esc_url_raw( $add_payment_method_url ) );
}
}
}
@@ -0,0 +1,357 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_PayPal_EC' ) ) {
/**
* Integrates with WooCommerce PayPal Checkout Payment Gateway,
* modifying that plugin's behavior to facilitate authenticating requests
* not by linking an account but via the WCS server through which we proxy.
*/
class WC_Connect_PayPal_EC {
/**
* @var WC_Connect_API_Client
*/
private $api_client;
/**
* @var WC_Connect_Nux
*/
private $nux;
/**
* Express Checkout API methods to proxy.
*/
private $methods_to_proxy = array( 'SetExpressCheckout', 'GetExpressCheckoutDetails', 'DoExpressCheckoutPayment' );
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Nux $nux ) {
$this->api_client = $api_client;
$this->nux = $nux;
}
public function init() {
if ( ! function_exists( 'wc_gateway_ppec' ) ) {
return;
}
$ppec_plugin = wc_gateway_ppec();
if ( ! property_exists( $ppec_plugin, 'settings' ) || empty( $ppec_plugin->settings ) ) {
return;
}
$this->maybe_set_reroute_requests();
add_filter( 'woocommerce_paypal_express_checkout_settings', array( $this, 'adjust_form_fields' ) );
$this->initialize_settings();
$settings = $ppec_plugin->settings;
// Don't modify any PPEC plugin behavior if WCS request proxying is not enabled
if ( 'yes' !== $settings->reroute_requests ) {
return;
}
// If empty, populate Sandbox and Live API Subject values with provided email
if (
empty( $settings->sandbox_api_subject ) &&
empty( $settings->sandbox_api_username ) &&
empty( $settings->api_username )
) {
$email = isset( $settings->email ) ? $settings->email : $settings->api_subject;
$settings->api_subject = $email;
$settings->sandbox_api_subject = $email;
$settings->save();
}
$username = $settings->get_active_api_credentials()->get_username();
$subject = $settings->get_active_api_credentials()->get_subject();
// Proceed to attach PPEC-related hooks if email address is present but credentials are missing
if ( empty( $username ) && ! empty( $subject ) ) {
add_filter( 'woocommerce_paypal_express_checkout_request_body', array( $this, 'request_body' ) );
add_filter( 'option_woocommerce_ppec_paypal_settings', array( $this, 'adjust_settings' ) );
add_filter( 'woocommerce_payment_gateway_supports', array( $this, 'ppec_supports' ), 10, 3 );
if ( 'live' === $settings->environment ) {
// If PPEC order comes in, activate prompt to connect a PayPal account
add_action( 'woocommerce_order_status_on-hold', array( $this, 'maybe_trigger_banner' ) );
add_action( 'woocommerce_payment_complete', array( $this, 'maybe_trigger_banner' ) );
// Once a payment is received, show prompt to connect a PayPal account on certain screens
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_show_banner' ) );
add_filter( 'wc_services_pointer_post.php', array( $this, 'register_refund_pointer' ) );
}
add_filter( 'pre_option_wc_gateway_ppce_prompt_to_connect', '__return_empty_string' ); // Disable default PPEC notice.
}
}
/**
* Attach request proxying hook if it's an Express Checkout method
*/
public function request_body( $body ) {
if ( in_array( $body['METHOD'], $this->methods_to_proxy ) ) {
add_filter( 'pre_http_request', array( $this, 'proxy_request' ), 10, 3 );
} else {
remove_filter( 'pre_http_request', array( $this, 'proxy_request' ), 10, 3 );
}
return $body;
}
/**
* Reroute Express Checkout requests from the PPEC extension via WCS server to pick up API credentials
*/
public function proxy_request( $preempt, $r, $url ) {
if ( ! preg_match( '/paypal.com\/nvp$/', $url ) ) {
return $preempt;
}
$settings = wc_gateway_ppec()->settings;
return $this->api_client->proxy_request( 'paypal/nvp/' . $settings->environment, $r );
}
/**
* Limit supported payment gateway features to payments alone
*/
public function ppec_supports( $supported, $feature, $gateway ) {
return 'ppec_paypal' === $gateway->id ? 'products' === $feature : $supported;
}
/**
* Add a pointer clarifying the need to link an account before refunding payment
*/
public function register_refund_pointer( $pointers ) {
$pointers[] = array(
'id' => 'wc_services_refund_via_ppec',
'target' => '.refund-actions > button:first-child',
'options' => array(
'content' => sprintf(
'<h3>%s</h3><p>%s</p>',
__( 'Link a PayPal account', 'woocommerce-services' ),
sprintf(
wp_kses(
__( 'To issue refunds via PayPal Checkout, you will need to <a href="%s">link a PayPal account</a> with the email address that received this payment.', 'woocommerce-services' ),
array( 'a' => array( 'href' => array() ) )
),
wc_gateway_ppec()->ips->get_signup_url( wc_gateway_ppec()->settings->environment )
)
),
'position' => array(
'edge' => 'bottom',
'align' => 'top',
),
),
'delayed_opening' => array(
'show_button' => '.refund-items',
'hide_button' => '.cancel-action',
'animating_container' => '.wc-order-refund-items',
'delegation_container' => '#woocommerce-order-items',
),
);
return $pointers;
}
/**
* Trigger banner to appear based on order paid with PPEC
*/
public function maybe_trigger_banner( $order_id ) {
$order = wc_get_order( $order_id );
$payment_method = $order ? $order->get_payment_method() : false;
if ( 'ppec_paypal' === $payment_method ) {
WC_Connect_Options::update_option( 'banner_ppec', 'yes' );
}
}
/**
* Show banner if it has been triggered and if this screen is an appropriate place for it
*/
public function maybe_show_banner() {
if ( 'yes' !== WC_Connect_Options::get_option( 'banner_ppec', null ) ) {
return;
}
$screen = get_current_screen();
$order = wc_get_order();
$payment_method = $order ? $order->get_payment_method() : false;
if ( // Display if on any of these admin pages.
( // Orders list.
'shop_order' === $screen->post_type
&& 'edit' === $screen->base
)
|| ( // Edit order page.
'shop_order' === $screen->post_type
&& 'post' === $screen->base
&& 'ppec_paypal' === $payment_method
)
|| ( // WooCommerce » Settings » Payments.
'woocommerce_page_wc-settings' === $screen->base
&& isset( $_GET['tab'] ) && 'checkout' === $_GET['tab']
)
|| ( // WooCommerce » Extensions » Payments.
'woocommerce_page_wc-addons' === $screen->base
&& isset( $_GET['section'] ) && 'payment-gateways' === $_GET['section']
)
) {
wp_enqueue_style( 'wc_connect_banner' );
add_action( 'admin_notices', array( $this, 'banner' ) );
}
}
/**
* Show a NUX banner prompting the merchant to link a PayPal account
*/
public function banner() {
$this->nux->show_nux_banner(
array(
'title' => __( 'Link your PayPal account', 'woocommerce-services' ),
'description' => esc_html( __( 'Link a new or existing PayPal account to make sure future orders are marked “Processing” instead of “On hold”, and so refunds can be issued without leaving WooCommerce.', 'woocommerce-services' ) ),
'button_text' => __( 'Link account', 'woocommerce-services' ),
'button_link' => wc_gateway_ppec()->ips->get_signup_url( 'live' ),
'image_url' => plugins_url( 'images/cashier.svg', dirname( __FILE__ ) ),
'should_show_jp' => false,
'dismissible_id' => 'ppec',
)
);
}
/**
* Initialize PPEC settings to their default values
*/
public function initialize_settings() {
$settings = get_option( 'woocommerce_ppec_paypal_settings', array() );
if ( ! isset( $settings['reroute_requests'] ) ) {
$settings['reroute_requests'] = 'no';
} elseif ( 'no' === $settings['reroute_requests'] ) {
return;
} elseif ( ! isset( $settings['button_size'] ) ) { // Check if settings are initialized, represented by button_size as its absence would be first to affect the customer
$payment_gateways = WC()->payment_gateways->payment_gateways();
$gateway = $payment_gateways['ppec_paypal'];
foreach ( $gateway->form_fields as $key => $form_field ) {
if ( ! isset( $settings[ $key ] ) && isset( $form_field['default'] ) ) {
$settings[ $key ] = $form_field['default'];
}
}
}
update_option( 'woocommerce_ppec_paypal_settings', $settings );
wc_gateway_ppec()->settings->load( true );
}
/**
* Force setting values that will work when proxying requests
*/
public function adjust_settings( $settings ) {
$settings['paymentaction'] = 'sale';
return $settings;
}
/**
* Modify PPEC settings form to include a toggle (and other accommodations) for WCS request proxying
*/
public function adjust_form_fields( $form_fields ) {
$settings = wc_gateway_ppec()->settings;
// Modify form fields and descriptions depending on whether WCS request proxying is enabled
if ( 'yes' === $settings->reroute_requests ) {
$form_fields = $this->adjust_api_subject_form_field( $form_fields );
// Prevent user from changing Payment Action away from "Sale", the only option for which payments will work
$form_fields['paymentaction']['disabled'] = true;
$form_fields['paymentaction']['description'] = sprintf( __( '%s (Note that "authorizing payment only" requires linking a PayPal account.)', 'woocommerce-services' ), $form_fields['paymentaction']['description'] );
// Communicate WCS proxying and provide option to disable
$reset_link = add_query_arg(
array(
'reroute_requests' => 'no',
'nonce' => wp_create_nonce( 'reroute_requests' ),
),
wc_gateway_ppec()->get_admin_setting_link()
);
$api_creds_template = __( 'Payments will be authenticated by WooCommerce Shipping & Tax and directed to the following email address. To disable this feature and link a PayPal account, <a href="%s">click here</a>.', 'woocommerce-services' );
if ( empty( $settings->api_username ) ) {
$api_creds_text = sprintf( $api_creds_template, esc_url( add_query_arg( 'environment', 'live', $reset_link ) ) );
$form_fields['api_credentials']['description'] = $api_creds_text;
unset( $form_fields['api_username'], $form_fields['api_password'], $form_fields['api_signature'], $form_fields['api_certificate'] );
}
if ( empty( $settings->sandbox_api_username ) ) {
$api_creds_text = sprintf( $api_creds_template, esc_url( add_query_arg( 'environment', 'sandbox', $reset_link ) ) );
$form_fields['sandbox_api_credentials']['description'] = $api_creds_text;
unset( $form_fields['sandbox_api_username'], $form_fields['sandbox_api_password'], $form_fields['sandbox_api_signature'], $form_fields['sandbox_api_certificate'] );
}
} else {
// Provide option to enable request proxying
$reset_link = add_query_arg(
array(
'reroute_requests' => 'yes',
'nonce' => wp_create_nonce( 'reroute_requests' ),
),
wc_gateway_ppec()->get_admin_setting_link()
);
$api_creds_template = __( 'To authenticate payments with WooCommerce Shipping & Tax, <a href="%s">click here</a>.', 'woocommerce-services' );
if ( empty( $settings->api_username ) ) {
$api_creds_text = sprintf( $api_creds_template, esc_url( add_query_arg( 'environment', 'live', $reset_link ) ) );
$form_fields['api_credentials']['description'] .= '<br /><br />' . $api_creds_text;
}
if ( empty( $settings->sandbox_api_username ) ) {
$api_creds_text = sprintf( $api_creds_template, esc_url( add_query_arg( 'environment', 'sandbox', $reset_link ) ) );
$form_fields['sandbox_api_credentials']['description'] .= '<br /><br />' . $api_creds_text;
}
}
return $form_fields;
}
/**
* Present the "API Subject" setting in a way that's simpler, more comprehensible, and more appropriate to the way it's being used
*/
public function adjust_api_subject_form_field( $form_fields ) {
$api_subject_title = __( 'Payment Email', 'woocommerce-services' );
$form_fields['api_subject']['title'] = $api_subject_title;
$form_fields['sandbox_api_subject']['title'] = $api_subject_title;
$api_subject_description = __( 'Enter your email address at which to accept payments. You\'ll need to link your own account in order to perform anything other than "sale" transactions.', 'woocommerce-services' );
$form_fields['api_subject']['description'] = $api_subject_description;
$form_fields['sandbox_api_subject']['description'] = $api_subject_description;
$api_subject_placeholder = __( 'Required', 'woocommerce-services' );
$form_fields['api_subject']['placeholder'] = $api_subject_placeholder;
$form_fields['sandbox_api_subject']['placeholder'] = $api_subject_placeholder;
return $form_fields;
}
/**
* Handle reroute_requests setting change
*/
public function maybe_set_reroute_requests() {
if (
! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ||
empty( $_GET['reroute_requests'] ) ||
empty( $_GET['nonce'] ) ||
! wp_verify_nonce( $_GET['nonce'], 'reroute_requests' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
) {
return;
}
$settings = wc_gateway_ppec()->settings;
$settings->reroute_requests = 'yes' === $_GET['reroute_requests'] ? 'yes' : 'no';
if ( isset( $_GET['environment'] ) ) {
$settings->environment = 'sandbox' === $_GET['environment'] ? 'sandbox' : 'live';
}
$settings->save();
wp_safe_redirect( wc_gateway_ppec()->get_admin_setting_link() );
exit;
}
}
}
@@ -0,0 +1,125 @@
<?php
if ( class_exists( 'WC_Connect_Privacy' ) ) {
return;
}
class WC_Connect_Privacy {
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
public function __construct( WC_Connect_Service_Settings_Store $settings_store, WC_Connect_API_Client $api_client ) {
$this->settings_store = $settings_store;
$this->api_client = $api_client;
add_action( 'admin_init', array( $this, 'add_privacy_message' ) );
add_action( 'admin_notices', array( $this, 'add_erasure_notice' ) );
add_filter( 'woocommerce_privacy_export_order_personal_data', array( $this, 'label_data_exporter' ), 10, 2 );
add_action( 'woocommerce_privacy_before_remove_order_personal_data', array( $this, 'label_data_eraser' ) );
}
/**
* Gets the privacy message to display in the admin panel
*/
public function add_privacy_message() {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$title = __( 'WooCommerce Shipping & Tax', 'woocommerce-services' );
$content = wpautop(
sprintf(
wp_kses(
__( 'By using this extension, you may be storing personal data or sharing data with external services. <a href="%s" target="_blank">Learn more about how this works, including what you may want to include in your privacy policy.</a>', 'woocommerce-services' ),
array(
'a' => array(
'href' => array(),
'target' => array(),
),
)
),
'https://jetpack.com/support/for-your-privacy-policy/#woocommerce-services'
)
);
wp_add_privacy_policy_content( $title, $content );
}
/**
* If WooCommerce order data erasure is enabled, display a warning on the erasure page
*/
public function add_erasure_notice() {
$screen = get_current_screen();
if ( 'tools_page_remove_personal_data' !== $screen->id ) {
return;
}
$erasure_enabled = wc_string_to_bool( get_option( 'woocommerce_erasure_request_removes_order_data', 'no' ) );
if ( ! $erasure_enabled ) {
return;
}
?>
<div class="notice notice-warning" style="position: relative;">
<p><?php esc_html_e( 'Warning: Erasing personal data will cause the ability to reprint or refund WooCommerce Shipping & Tax shipping labels to be lost on the affected orders.', 'woocommerce-services' ); ?></p>
</div>
<?php
}
/**
* Filter for woocommerce_privacy_export_order_personal_data that adds WCS personal data to the exported orders
*
* @param array $personal_data
* @param object $order
* @return array
*/
public function label_data_exporter( $personal_data, $order ) {
$order_id = $order->get_id();
$labels = $this->settings_store->get_label_order_meta_data( $order_id );
foreach ( $labels as $label ) {
if ( empty( $label['tracking'] ) ) {
continue;
}
$personal_data[] = array(
'name' => __( 'Shipping label service', 'woocommerce-services' ),
'value' => $label['service_name'],
);
$personal_data[] = array(
'name' => __( 'Shipping label tracking number', 'woocommerce-services' ),
'value' => $label['tracking'],
);
}
return $personal_data;
}
/**
* Hooks into woocommerce_privacy_before_remove_order_personal_data to remove WCS personal data from orders
*
* @param WC_Order $order WC Order.
*/
public function label_data_eraser( $order ) {
$order_id = $order->get_id();
$labels = $this->settings_store->get_label_order_meta_data( $order_id );
if ( empty( $labels ) ) {
return;
}
foreach ( $labels as $label_idx => $label ) {
$labels[ $label_idx ]['tracking'] = '';
$labels[ $label_idx ]['status'] = 'ANONYMIZED';
}
$this->api_client->anonymize_order( $order_id );
$order->update_meta_data( 'wc_connect_labels', $labels );
$order->save();
}
}
@@ -0,0 +1,291 @@
<?php
if ( ! class_exists( 'WC_Connect_Service_Schemas_Store' ) ) {
class WC_Connect_Service_Schemas_Store {
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* @var WC_Connect_Logger
*/
protected $logger;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Logger $logger ) {
$this->api_client = $api_client;
$this->logger = $logger;
}
public function fetch_service_schemas_from_connect_server() {
$response_body = $this->api_client->get_service_schemas();
if ( is_wp_error( $response_body ) ) {
$error_data = $response_body->get_error_data();
if ( isset( $error_data['response_status_code'] ) ) {
$this->update_last_fetch_result_code( $error_data['response_status_code'] );
}
$this->logger->log( $response_body, __FUNCTION__ );
return false;
}
$this->update_last_fetch_result_code( '200' );
$this->logger->log( 'Successfully loaded service schemas from server response.', __FUNCTION__ );
$this->update_last_fetch_timestamp();
$this->maybe_update_heartbeat();
$old_schemas = $this->get_service_schemas();
if ( $old_schemas == $response_body ) {
// schemas weren't changed, but were fetched without problems
return true;
}
// If we made it this far, it is safe to store the object
return $this->update_service_schemas( $response_body );
}
public function get_service_schemas() {
return WC_Connect_Options::get_option( 'services', null );
}
protected function update_service_schemas( $service_schemas ) {
return WC_Connect_Options::update_option( 'services', $service_schemas );
}
public function get_last_fetch_timestamp() {
return WC_Connect_Options::get_option( 'services_last_update', null );
}
protected function update_last_fetch_timestamp() {
WC_Connect_Options::update_option( 'services_last_update', time() );
}
public function get_last_fetch_result_code() {
return WC_Connect_Options::get_option( 'services_last_result_code' );
}
/**
* @param int $result_status_code
*/
protected function update_last_fetch_result_code( $result_status_code ) {
WC_Connect_Options::update_option( 'services_last_result_code', $result_status_code );
}
protected function maybe_update_heartbeat() {
$last_heartbeat = WC_Connect_Options::get_option( 'last_heartbeat' );
$now = time();
if ( ! $last_heartbeat ) {
$should_update = true;
} else {
$last_heartbeat = absint( $last_heartbeat );
if ( $last_heartbeat > $now ) {
// last heartbeat in the future? wacky
$should_update = true;
} else {
$elapsed = $now - $last_heartbeat;
$should_update = $elapsed > DAY_IN_SECONDS;
}
}
if ( $should_update ) {
WC_Connect_Options::update_option( 'last_heartbeat', $now );
}
}
/**
* Returns all service ids of a specific type (e.g. shipping)
*
* @param string $type The type of services to return
*
* @return array An array of that type's service ids, or an empty array if no such type is known
*/
public function get_all_service_ids_of_type( $type ) {
if ( empty( $type ) ) {
return array();
}
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) || ! property_exists( $service_schemas, $type ) || ! is_array( $service_schemas->$type ) ) {
return array();
}
$service_schema_ids = array();
foreach ( $service_schemas->$type as $service_schema ) {
$service_schema_ids[] = $service_schema->id;
}
return $service_schema_ids;
}
/**
* Returns all shipping method ids
*
* @return array|bool An array of supported shipping method ids or false if schema does not support method_id
*/
public function get_all_shipping_method_ids() {
$shipping_method_ids = array();
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) || ! property_exists( $service_schemas, 'shipping' ) || ! is_array( $service_schemas->shipping ) ) {
return $shipping_method_ids;
}
foreach ( $service_schemas->shipping as $service_schema ) {
if ( ! property_exists( $service_schema, 'method_id' ) ) {
continue;
}
$shipping_method_ids[] = $service_schema->method_id;
}
return $shipping_method_ids;
}
/**
* Returns a particular service's schema given its id
*
* @param string $service_id The service id for which to return the schema
*
* @return object|null The service schema or null if no such id was found
*/
public function get_service_schema_by_id( $service_id ) {
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) ) {
return null;
}
foreach ( $service_schemas as $service_type => $service_type_service_schemas ) {
$matches = wp_filter_object_list( $service_type_service_schemas, array( 'id' => $service_id ) );
if ( $matches ) {
return array_shift( $matches );
}
}
return null;
}
/**
* Returns a particular service's schema given its method_id
*
* @param $method_id
*
* @return object|null The service schema or null if no such id was found
*/
public function get_service_schema_by_method_id( $method_id ) {
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) ) {
return null;
}
foreach ( $service_schemas as $service_type => $service_type_service_schemas ) {
$matches = wp_filter_object_list( $service_type_service_schemas, array( 'method_id' => $method_id ) );
if ( $matches ) {
return array_shift( $matches );
}
}
return null;
}
/**
* Returns a service's schema given its shipping zone instance
*
* @param string $instance_id The shipping zone instance id for which to return the schema
*
* @return object|null The service schema or null if no such instance was found
*/
public function get_service_schema_by_instance_id( $instance_id ) {
global $wpdb;
$method_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d;",
$instance_id
)
);
return $this->get_service_schema_by_method_id( $method_id );
}
/**
* Returns a service's schema given an id or shipping zone instance.
*
* @param string $id_or_instance_id String ID or numeric instance ID.
* @return object|null Service schema on success, null on failure
*/
public function get_service_schema_by_id_or_instance_id( $id_or_instance_id ) {
if ( is_numeric( $id_or_instance_id ) ) {
return $this->get_service_schema_by_instance_id( $id_or_instance_id );
}
if ( ! empty( $id_or_instance_id ) ) {
return $this->get_service_schema_by_method_id( $id_or_instance_id );
}
return null;
}
/**
* Returns packages schema
*
* @return object|null Packages schema on success, null on failure
*/
public function get_packages_schema() {
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) || ! property_exists( $service_schemas, 'boxes' ) ) {
return null;
}
return $service_schemas->boxes;
}
public function get_predefined_packages_schema() {
$service_schemas = $this->get_service_schemas();
if ( ! is_object( $service_schemas ) ) {
return null;
}
$predefined_packages = array();
foreach ( $service_schemas->shipping as $service_schema ) {
if ( ! isset( $service_schema->packages ) ) {
continue;
}
$predefined_packages[ $service_schema->id ] = $service_schema->packages;
}
return $predefined_packages;
}
/**
* Returns the WooCommerce Shipping and WooCommerce Tax migration enabled status
*
* @return bool
*/
public function is_wcship_wctax_migration_enabled() {
$service_schemas = $this->get_service_schemas();
return is_object( $service_schemas ) && property_exists( $service_schemas, 'features' ) && property_exists( $service_schemas->features, 'wcshippingtax_upgrade_banner' ) && ! empty( $service_schemas->features->wcshippingtax_upgrade_banner );
}
/**
* Returns the WooCommerce Shipping and WooCommerce Tax upgrade banners
*
* @return object|null The banners schema or null if no such id was found
*/
public function get_wcship_wctax_upgrade_banner() {
$service_schemas = $this->get_service_schemas();
if ( $this->is_wcship_wctax_migration_enabled() ) {
return $service_schemas->features->wcshippingtax_upgrade_banner;
}
return null;
}
}
}
@@ -0,0 +1,195 @@
<?php
if ( ! class_exists( 'WC_Connect_Service_Schemas_Validator' ) ) {
class WC_Connect_Service_Schemas_Validator {
/**
* Validates the overall passed services object (all service types and all services therein)
*
* @param object $services
*
* @return WP_Error|true
*/
public function validate_service_schemas( $service_schemas ) {
if ( ! is_object( $service_schemas ) ) {
return new WP_Error(
'outermost_container_not_object',
'Malformed service schemas. Outermost container is not an object.'
);
}
if ( ! isset( $service_schemas->shipping ) || ! is_array( $service_schemas->shipping ) ) {
return new WP_Error(
'service_type_not_ref_array',
'Malformed service schemas. \'shipping\' does not reference an array.'
);
}
$service_counter = 0;
foreach ( $service_schemas->shipping as $service_schema ) {
if ( ! is_object( $service_schema ) ) {
return new WP_Error(
'service_not_ref_object',
sprintf(
'Malformed service schema. Service type \'shipping\' [%d] does not reference an object.',
$service_counter
)
);
}
$result = $this->validate_service_schema( 'shipping', $service_counter, $service_schema );
if ( is_wp_error( $result ) ) {
return $result;
}
$service_counter ++;
}
if ( ! isset( $service_schemas->boxes ) || ! is_object( $service_schemas->boxes ) ) {
return new WP_Error(
'boxes_not_object',
'Malformed service schemas. \'boxes\' is not an object.'
);
}
return true;
}
/**
* Validates a particular service schema, especially the parts of the service that WC relies
* on like id, method_title, method_description, etc
*
* @param string $service_type
* @param integer $service_counter
* @param object $service
*
* @return WP_Error|true
*/
protected function validate_service_schema( $service_type, $service_counter, $service_schema ) {
$required_properties = array(
'id' => 'string',
'method_description' => 'string',
'method_title' => 'string',
'service_settings' => 'object',
'form_layout' => 'array',
);
foreach ( $required_properties as $required_property => $required_property_type ) {
if ( ! property_exists( $service_schema, $required_property ) ) {
return new WP_Error(
'required_service_property_missing',
sprintf(
'Malformed service schema. Service type \'%s\' [%d] does not include a required \'%s\' property.',
$service_type,
$service_counter,
$required_property
)
);
}
$property_type = gettype( $service_schema->$required_property );
if ( $required_property_type !== $property_type ) {
return new WP_Error(
'required_service_property_wrong_type',
sprintf(
'Malformed service schema. Service type \'%s\' [%d] property \'%s\' is a %s. Was expecting a %s.',
$service_type,
$service_counter,
$service_schema->$required_property,
$property_type,
$required_property_type
)
);
}
}
return $this->validate_service_schema_settings( $service_schema->id, $service_schema->service_settings );
}
/**
* Validates a particular service's service settings schema, especially the parts of the
* service settings that WC relies on like type, required and properties
*
* @param string $service_id
* @param object $service_settings
*
* @return WP_Error|true
*/
protected function validate_service_schema_settings( $service_id, $service_settings ) {
$required_properties = array(
'type' => 'string',
'required' => 'array',
'properties' => 'object',
);
foreach ( $required_properties as $required_property => $required_property_type ) {
if ( ! property_exists( $service_settings, $required_property ) ) {
return new WP_Error(
'service_settings_missing_required_property',
sprintf(
'The settings part of a service schema is malformed. Service \'%s\' service_settings do not include a required \'%s\' property.',
$service_id,
$required_property
)
);
}
$property_type = gettype( $service_settings->$required_property );
if ( $required_property_type !== $property_type ) {
return new WP_Error(
'service_settings_property_wrong_type',
sprintf(
"The settings part of a service schema is malformed. Service '%s' service_setting property '%s' is a %s. Was expecting a %s.",
$service_id,
$required_property,
$property_type,
$required_property_type
)
);
}
}
$result = $this->validate_service_settings_required_properties( $service_id, $service_settings->properties );
if ( is_wp_error( $result ) ) {
return $result;
}
return true;
}
/**
* Validates a particular service's schema's required properties, especially the parts of the
* properties that WC relies on and title
*
* @param string $service_id
* @param object $service_settings_properties
*
* @return WP_Error|true
*/
protected function validate_service_settings_required_properties( $service_id, $service_settings_properties ) {
$required_properties = array(
'title',
);
foreach ( $required_properties as $required_property ) {
if ( ! property_exists( $service_settings_properties, $required_property ) ) {
return new WP_Error(
'service_properties_missing_required_property',
sprintf(
"The properties part of a service schema is malformed. Service '%s' service_settings properties do not include a required '%s' property.",
$service_id,
$required_property
)
);
}
}
return true;
}
}
}
@@ -0,0 +1,670 @@
<?php
if ( ! class_exists( 'WC_Connect_Service_Settings_Store' ) ) {
class WC_Connect_Service_Settings_Store {
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* @var WC_Connect_Logger
*/
protected $logger;
public function __construct( WC_Connect_Service_Schemas_Store $service_schemas_store, WC_Connect_API_Client $api_client, WC_Connect_Logger $logger ) {
$this->service_schemas_store = $service_schemas_store;
$this->api_client = $api_client;
$this->logger = $logger;
}
/**
* Gets woocommerce store options that are useful for all connect services
*
* @return object|array
*/
public function get_store_options() {
$currency_symbol = sanitize_text_field( html_entity_decode( get_woocommerce_currency_symbol() ) );
$dimension_unit = sanitize_text_field( strtolower( get_option( 'woocommerce_dimension_unit' ) ) );
$weight_unit = sanitize_text_field( strtolower( get_option( 'woocommerce_weight_unit' ) ) );
$base_location = wc_get_base_location();
return array(
'currency_symbol' => $currency_symbol,
'dimension_unit' => $this->translate_unit( $dimension_unit ),
'weight_unit' => $this->translate_unit( $weight_unit ),
'origin_country' => $base_location['country'],
);
}
/**
* Gets connect account settings (e.g. payment method)
*
* @return array
*/
public function get_account_settings() {
$default = array(
'selected_payment_method_id' => 0,
'enabled' => true,
);
$result = WC_Connect_Options::get_option( 'account_settings', $default );
$result['paper_size'] = $this->get_preferred_paper_size();
$result = array_merge( $default, $result );
if ( ! isset( $result['email_receipts'] ) ) {
$result['email_receipts'] = true;
}
if ( ! isset( $result['use_last_service'] ) ) {
$result['use_last_service'] = false;
}
if ( ! isset( $result['use_last_package'] ) ) {
$result['use_last_package'] = true;
}
return $result;
}
/**
* Updates connect account settings (e.g. payment method)
*
* @param array $settings
*
* @return true
*/
public function update_account_settings( $settings ) {
// simple validation for now.
if ( ! is_array( $settings ) ) {
$this->logger->log( 'Array expected but not received', __FUNCTION__ );
return false;
}
$paper_size = $settings['paper_size'];
$this->set_preferred_paper_size( $paper_size );
unset( $settings['paper_size'] );
return WC_Connect_Options::update_option( 'account_settings', $settings );
}
public function get_selected_payment_method_id() {
$account_settings = $this->get_account_settings();
return intval( $account_settings['selected_payment_method_id'] );
}
public function set_selected_payment_method_id( $new_payment_method_id ) {
$new_payment_method_id = intval( $new_payment_method_id );
$account_settings = $this->get_account_settings();
$old_payment_method_id = intval( $account_settings['selected_payment_method_id'] );
if ( $old_payment_method_id === $new_payment_method_id ) {
return;
}
$account_settings['selected_payment_method_id'] = $new_payment_method_id;
$this->update_account_settings( $account_settings );
}
public function can_user_manage_payment_methods() {
return WC_Connect_Jetpack::is_offline_mode() || WC_Connect_Jetpack::is_current_user_connection_owner();
}
public function get_origin_address() {
$wc_address_fields = array();
$wc_address_fields['company'] = html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES ); // HTML entities may be saved in the option.
$wc_address_fields['name'] = wp_get_current_user()->display_name;
$wc_address_fields['phone'] = '';
$wc_countries = WC()->countries;
// WC 3.2 introduces ability to configure a full address in the settings
// Use it for address defaults if available
if ( method_exists( $wc_countries, 'get_base_address' ) ) {
$wc_address_fields['country'] = $wc_countries->get_base_country();
$wc_address_fields['state'] = $wc_countries->get_base_state();
$wc_address_fields['address'] = $wc_countries->get_base_address();
$wc_address_fields['address_2'] = $wc_countries->get_base_address_2();
$wc_address_fields['city'] = $wc_countries->get_base_city();
$wc_address_fields['postcode'] = $wc_countries->get_base_postcode();
} else {
$base_location = wc_get_base_location();
$wc_address_fields['country'] = $base_location['country'];
$wc_address_fields['state'] = $base_location['state'];
$wc_address_fields['address'] = '';
$wc_address_fields['address_2'] = '';
$wc_address_fields['city'] = '';
$wc_address_fields['postcode'] = '';
}
$stored_address_fields = WC_Connect_Options::get_option( 'origin_address', array() );
$merged_fields = is_array( $stored_address_fields ) ? array_merge( $wc_address_fields, $stored_address_fields ) : $wc_address_fields;
$merged_fields['company'] = html_entity_decode( $merged_fields['company'], ENT_QUOTES ); // Decode again for any existing stores that had some html entities saved in the option.
return $merged_fields;
}
public function get_preferred_paper_size() {
$paper_size = WC_Connect_Options::get_option( 'paper_size', '' );
if ( $paper_size ) {
return $paper_size;
}
// According to https://en.wikipedia.org/wiki/Letter_(paper_size) US, Mexico, Canada and Dominican Republic
// use "Letter" size, and pretty much all the rest of the world use A4, so those are sensible defaults.
$base_location = wc_get_base_location();
if ( in_array( $base_location['country'], array( 'US', 'CA', 'MX', 'DO' ), true ) ) {
return 'letter';
}
return 'a4';
}
public function set_preferred_paper_size( $size ) {
return WC_Connect_Options::update_option( 'paper_size', $size );
}
/**
* Attempts to recover faulty json string fields that might contain strings with unescaped quotes
*
* @param string $field_name
* @param string $json
*
* @return string
*/
public function try_recover_invalid_json_string( $field_name, $json ) {
$regex = '/"' . $field_name . '":"(.+?)","/';
preg_match_all( $regex, $json, $match_groups );
if ( 2 === count( $match_groups ) ) {
foreach ( $match_groups[0] as $idx => $match ) {
$value = $match_groups[1][ $idx ];
$escaped_value = preg_replace( '/(?<!\\\)"/', '\\"', $value );
$json = str_replace( $match, '"' . $field_name . '":"' . $escaped_value . '","', $json );
}
}
return $json;
}
/**
* Attempts to recover faulty json string array fields that might contain strings with unescaped quotes
*
* @param string $field_name
* @param string $json
*
* @return string
*/
public function try_recover_invalid_json_array( $field_name, $json ) {
$regex = '/"' . $field_name . '":\["(.+?)"\]/';
preg_match_all( $regex, $json, $match_groups );
if ( 2 === count( $match_groups ) ) {
foreach ( $match_groups[0] as $idx => $match ) {
$array = $match_groups[1][ $idx ];
$escaped_array = preg_replace( '/(?<![,\\\])"(?!,)/', '\\"', $array );
$json = str_replace( '["' . $array . '"]', '["' . $escaped_array . '"]', $json );
}
}
return $json;
}
public function try_deserialize_labels_json( $label_data ) {
// attempt to decode the JSON (legacy way of storing the labels data).
$decoded_labels = json_decode( $label_data, true );
if ( $decoded_labels ) {
return $decoded_labels;
}
$label_data = $this->try_recover_invalid_json_string( 'package_name', $label_data );
$decoded_labels = json_decode( $label_data, true );
if ( $decoded_labels ) {
return $decoded_labels;
}
$label_data = $this->try_recover_invalid_json_array( 'product_names', $label_data );
$decoded_labels = json_decode( $label_data, true );
if ( $decoded_labels ) {
return $decoded_labels;
}
return array();
}
/**
* Returns labels for the specific order ID
*
* @param $order_id
*
* @return array
*/
public function get_label_order_meta_data( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return array();
}
$label_data = $order->get_meta( 'wc_connect_labels', true );
// return an empty array if the data doesn't exist.
if ( ! $label_data ) {
return array();
}
// labels stored as an array, return.
if ( is_array( $label_data ) ) {
return $label_data;
}
return $this->try_deserialize_labels_json( $label_data );
}
/**
* Updates the existing label data
*
* @param $order_id
* @param $new_label_data
*
* @return array updated label info
*/
public function update_label_order_meta_data( $order_id, $new_label_data ) {
$result = $new_label_data;
$order = wc_get_order( $order_id );
$labels_data = $this->get_label_order_meta_data( $order_id );
foreach ( $labels_data as $index => $label_data ) {
if ( $label_data['label_id'] === $new_label_data->label_id ) {
$result = array_merge( $label_data, (array) $new_label_data );
$labels_data[ $index ] = $result;
if ( ! isset( $label_data['tracking'] )
&& isset( $result['tracking'] ) ) {
WC_Connect_Extension_Compatibility::on_new_tracking_number( $order_id, $result['carrier_id'], $result['tracking'] );
}
}
}
$order->update_meta_data( 'wc_connect_labels', $labels_data );
$order->save();
return $result;
}
/**
* Adds new labels to the order
*
* @param $order_id
* @param array $new_labels - labels to be added
*/
public function add_labels_to_order( $order_id, $new_labels ) {
$labels_data = $this->get_label_order_meta_data( $order_id );
$labels_data = array_merge( $new_labels, $labels_data );
$order = wc_get_order( $order_id );
$order->update_meta_data( 'wc_connect_labels', $labels_data );
$order->save();
}
public function update_origin_address( $address ) {
return WC_Connect_Options::update_option( 'origin_address', $address );
}
public function update_destination_address( $order_id, $api_address ) {
$order = wc_get_order( $order_id );
$wc_address = $order->get_address( 'shipping' );
$new_address = array_merge( array(), (array) $wc_address, (array) $api_address );
// rename address to address_1.
$new_address['address_1'] = $new_address['address'];
// remove api-specific fields.
unset( $new_address['address'], $new_address['name'] );
foreach ( $new_address as $key => $value ) {
if ( method_exists( $order, 'set_shipping_' . $key ) ) {
call_user_func( array( $order, 'set_shipping_' . $key ), $value );
}
}
$order->update_meta_data( '_wc_connect_destination_normalized', true );
$order->save();
}
protected function sort_services( $a, $b ) {
if ( $a->zone_order === $b->zone_order ) {
return ( $a->instance_id > $b->instance_id ) ? 1 : -1;
}
if ( is_null( $a->zone_order ) ) {
return 1;
}
if ( is_null( $b->zone_order ) ) {
return -1;
}
return ( $a->instance_id > $b->instance_id ) ? 1 : -1;
}
/**
* Returns the service type and id for each enabled WooCommerce Shipping & Tax service
*
* Shipping services also include instance_id and shipping zone id
*
* Note that at this time, only shipping services exist, but this method will
* return other services in the future
*
* @return array
*/
public function get_enabled_services() {
$shipping_services = $this->service_schemas_store->get_all_shipping_method_ids();
if ( empty( $shipping_services ) ) {
return array();
}
return $this->get_enabled_services_by_ids( $shipping_services );
}
public function get_enabled_services_by_ids( $service_ids ) {
if ( empty( $service_ids ) ) {
return array();
}
$enabled_services = array();
// Note: We use esc_sql here instead of prepare because we are using WHERE IN
// https://codex.wordpress.org/Function_Reference/esc_sql.
$escaped_list = '';
foreach ( $service_ids as $shipping_service ) {
if ( ! empty( $escaped_list ) ) {
$escaped_list .= ',';
}
$escaped_list .= "'" . esc_sql( $shipping_service ) . "'";
}
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared --- Need to use interpolated for the `IN()` condition
$methods = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}woocommerce_shipping_zone_methods " .
"LEFT JOIN {$wpdb->prefix}woocommerce_shipping_zones " .
"ON {$wpdb->prefix}woocommerce_shipping_zone_methods.zone_id = {$wpdb->prefix}woocommerce_shipping_zones.zone_id " .
"WHERE method_id IN ({$escaped_list}) " .
'ORDER BY zone_order, instance_id;'
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( empty( $methods ) ) {
return $enabled_services;
}
foreach ( (array) $methods as $method ) {
$service_schema = $this->service_schemas_store->get_service_schema_by_method_id( $method->method_id );
$service_settings = $this->get_service_settings( $method->method_id, $method->instance_id );
if ( is_object( $service_settings ) && property_exists( $service_settings, 'title' ) ) {
$title = $service_settings->title;
} elseif ( is_object( $service_schema ) && property_exists( $service_schema, 'method_title' ) ) {
$title = $service_schema->method_title;
} else {
$title = _x( 'Unknown', 'A service with an unknown title and unknown method_title', 'woocommerce-services' );
}
$method->service_type = 'shipping';
$method->title = $title;
$method->zone_name = empty( $method->zone_name ) ? __( 'Rest of the World', 'woocommerce-services' ) : $method->zone_name;
$enabled_services[] = $method;
}
usort( $enabled_services, array( $this, 'sort_services' ) );
return $enabled_services;
}
/**
* Checks if the shipping method ids have been migrated to the "wc_services_*" format and migrates them
*/
public function migrate_legacy_services() {
if ( WC_Connect_Options::get_option( 'shipping_methods_migrated', false ) ) { // check if the method have already been migrated.
return;
}
if ( ! $this->service_schemas_store->fetch_service_schemas_from_connect_server() ) { // ensure the latest schemas are fetched.
// No schemes exist this is a site that has nothing to migrate.
WC_Connect_Options::update_option( 'shipping_methods_migrated', true );
return;
}
global $wpdb;
// old services used the id field instead of method_id.
$shipping_service_ids = $this->service_schemas_store->get_all_service_ids_of_type( 'shipping' );
$legacy_services = $this->get_enabled_services_by_ids( $shipping_service_ids );
foreach ( $legacy_services as $legacy_service ) {
$service_id = $legacy_service->method_id;
$instance_id = $legacy_service->instance_id;
$service_schema = $this->service_schemas_store->get_service_schema_by_id( $service_id );
$service_settings = $this->get_service_settings( $service_id, $instance_id );
if ( ( is_array( $service_settings ) && ! $service_settings ) // check for an empty array.
|| ( ! is_array( $service_settings ) && ! is_object( $service_settings ) ) ) { // settings are neither an array nor an object.
continue;
}
$new_method_id = $service_schema->method_id;
$wpdb->update(
"{$wpdb->prefix}woocommerce_shipping_zone_methods",
array( 'method_id' => $new_method_id ),
array(
'instance_id' => $instance_id,
'method_id' => $service_id,
),
array( '%s' ),
array( '%d', '%s' )
);
// update the migrated service settings.
WC_Connect_Options::update_shipping_method_option( 'form_settings', $service_settings, $new_method_id, $instance_id );
// delete the old service settings.
WC_Connect_Options::delete_shipping_method_options( $service_id, $instance_id );
}
WC_Connect_Options::update_option( 'shipping_methods_migrated', true );
}
/**
* Given a service's id and optional instance, returns the settings for that
* service or an empty array
*
* @param string $service_id
* @param integer $service_instance
*
* @return object|array
*/
public function get_service_settings( $service_id, $service_instance = false ) {
return WC_Connect_Options::get_shipping_method_option( 'form_settings', array(), $service_id, $service_instance );
}
/**
* Given id and possibly instance, validates the settings and, if they validate, saves them to options
*
* @return bool|WP_Error
*/
public function validate_and_possibly_update_settings( $settings, $id, $instance = false ) {
// Validate instance or at least id if no instance is given.
if ( ! empty( $instance ) ) {
$service_schema = $this->service_schemas_store->get_service_schema_by_instance_id( $instance );
if ( ! $service_schema ) {
return new WP_Error( 'bad_instance_id', __( 'An invalid service instance was received.', 'woocommerce-services' ) );
}
} else {
$service_schema = $this->service_schemas_store->get_service_schema_by_method_id( $id );
if ( ! $service_schema ) {
return new WP_Error( 'bad_service_id', __( 'An invalid service ID was received.', 'woocommerce-services' ) );
}
}
// Validate settings with WCC server.
$response_body = $this->api_client->validate_service_settings( $service_schema->id, $settings );
if ( is_wp_error( $response_body ) ) {
// TODO - handle multiple error messages when the validation endpoint can return them
return $response_body;
}
// On success, save the settings to the database and exit.
WC_Connect_Options::update_shipping_method_option( 'form_settings', $settings, $id, $instance );
// Invalidate shipping rates session cache.
WC_Cache_Helper::get_transient_version( 'shipping', /* $refresh = */ true );
do_action( 'wc_connect_saved_service_settings', $id, $instance, $settings );
return true;
}
/**
* Returns a global list of packages
*
* @return array
*/
public function get_packages() {
return WC_Connect_Options::get_option( 'packages', array() );
}
/**
* Extends the global list of packages with a list of new packages
*
* @param array new_packages - packages to extend
*/
public function create_packages( $new_packages ) {
if ( is_null( $new_packages ) ) {
return;
}
$packages = $this->get_packages();
$packages = array_merge( $packages, $new_packages );
WC_Connect_Options::update_option( 'packages', $packages );
}
/**
* Updates the global list of packages
*
* @param array packages
*/
public function update_packages( $packages ) {
WC_Connect_Options::update_option( 'packages', $packages );
}
/**
* Returns a global list of enabled predefined packages for all services
*
* @return array
*/
public function get_predefined_packages() {
return WC_Connect_Options::get_option( 'predefined_packages', array() );
}
/**
* Returns a list of enabled predefined packages for the specified service
*
* @param $service_id
* @return array
*/
public function get_predefined_packages_for_service( $service_id ) {
$packages = $this->get_predefined_packages();
if ( ! isset( $packages[ $service_id ] ) ) {
return array();
}
return $packages[ $service_id ];
}
/**
* Extends the global list of enabled predefined packages with a list of new packages
*
* @param array new_packages - packages to extend
*/
public function create_predefined_packages( $new_packages ) {
if ( is_null( $new_packages ) ) {
return;
}
$packages = $this->get_predefined_packages();
$packages = array_merge_recursive( $packages, $new_packages );
WC_Connect_Options::update_option( 'predefined_packages', $packages );
}
/**
* Updates the global list of enabled predefined packages for all services
*
* @param array packages
*/
public function update_predefined_packages( $packages ) {
WC_Connect_Options::update_option( 'predefined_packages', $packages );
}
public function get_package_lookup() {
$lookup = array();
$custom_packages = $this->get_packages();
foreach ( $custom_packages as $custom_package ) {
$lookup[ $custom_package['name'] ] = $custom_package;
}
$predefined_packages_schema = $this->service_schemas_store->get_predefined_packages_schema();
if ( is_null( $predefined_packages_schema ) ) {
return $lookup;
}
foreach ( $predefined_packages_schema as $service_id => $groups ) {
foreach ( $groups as $group ) {
foreach ( $group->definitions as $predefined ) {
$lookup[ $predefined->id ] = (array) $predefined;
}
}
}
return $lookup;
}
public function is_eligible_for_migration() {
$migration_state = intval( get_option( 'wcshipping_migration_state', 0 ) );
// If the migration state is greater than "COMPLETED", then we can assume that the next part of the migration
// state is being handled by WooCommerce Shipping.
if ( $migration_state > WC_Connect_WCST_To_WCShipping_Migration_State_Enum::COMPLETED ) {
return false;
}
// Hide the migration notification if the site has any active shipping methods defined by WCS&T.
if ( ! empty( $this->get_enabled_services() ) ) {
return false;
}
$migration_dismissed = false;
if ( isset( $_COOKIE[ WC_Connect_Loader::MIGRATION_DISMISSAL_COOKIE_KEY ] ) && (int) $_COOKIE[ WC_Connect_Loader::MIGRATION_DISMISSAL_COOKIE_KEY ] === 1 ) {
$migration_dismissed = true;
}
$migration_pending = ! $migration_state || WC_Connect_WCST_To_WCShipping_Migration_State_Enum::COMPLETED !== $migration_state;
$migration_enabled = $this->service_schemas_store->is_wcship_wctax_migration_enabled();
return $migration_pending && $migration_enabled && ! $migration_dismissed;
}
private function translate_unit( $value ) {
switch ( $value ) {
case 'kg':
return __( 'kg', 'woocommerce-services' );
case 'g':
return __( 'g', 'woocommerce-services' );
case 'lbs':
return __( 'lbs', 'woocommerce-services' );
case 'oz':
return __( 'oz', 'woocommerce-services' );
case 'm':
return __( 'm', 'woocommerce-services' );
case 'cm':
return __( 'cm', 'woocommerce-services' );
case 'mm':
return __( 'mm', 'woocommerce-services' );
case 'in':
return __( 'in', 'woocommerce-services' );
case 'yd':
return __( 'yd', 'woocommerce-services' );
default:
$this->logger->log( 'Unexpected measurement unit: ' . $value, __FUNCTION__ );
return $value;
}
}
}
}
@@ -0,0 +1,153 @@
<?php
if ( ! class_exists( 'WC_Connect_Settings_Pages' ) ) {
class WC_Connect_Settings_Pages {
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
/**
* @var WC_Connect_Continents
*/
protected $continents;
/**
* @var string;
*/
protected $id;
/**
* @var string;
*/
protected $label;
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Service_Schemas_Store $service_schemas_store ) {
$this->id = 'connect';
$this->label = _x( 'WooCommerce Shipping', 'The WooCommerce Shipping & Tax brandname', 'woocommerce-services' );
$this->continents = new WC_Connect_Continents();
$this->api_client = $api_client;
$this->service_schemas_store = $service_schemas_store;
add_filter( 'woocommerce_get_sections_shipping', array( $this, 'get_sections' ), 30 );
add_action( 'woocommerce_settings_shipping', array( $this, 'output_settings_screen' ), 5 );
}
/**
* Get sections.
*
* @return array
*/
public function get_sections( $shipping_tabs ) {
// If WC Shipping is active, it will register its page instead.
if ( WC_Connect_Loader::is_wc_shipping_activated() ) {
return $shipping_tabs;
}
if ( ! is_array( $shipping_tabs ) ) {
$shipping_tabs = array();
}
$shipping_tabs['woocommerce-services-settings'] = __( 'WooCommerce Shipping', 'woocommerce-services' );
return $shipping_tabs;
}
/**
* Output the settings.
*/
public function output_settings_screen() {
global $current_section;
if ( 'woocommerce-services-settings' !== $current_section ) {
return;
}
add_filter( 'woocommerce_get_settings_shipping', '__return_empty_array' );
$this->output_shipping_settings_screen();
}
/**
* Localizes the bootstrap, enqueues the script and styles for the settings page
*/
public function output_shipping_settings_screen() {
// hiding the save button because the react container has its own.
global $hide_save_button;
$hide_save_button = true;
if ( WC_Connect_Jetpack::is_offline_mode() ) {
if ( WC_Connect_Jetpack::is_connected() ) {
$message = __( 'Note: Your site is connected but WooCommerce Shipping & Tax is configured to work in offline mode. Please disable offline mode.', 'woocommerce-services' );
} else {
$message = __( 'Note: WooCommerce Shipping & Tax is configured to work in offline mode. This site will not be able to obtain payment methods from WooCommerce Shipping & Tax production servers.', 'woocommerce-services' );
}
?>
<div class="wc-connect-admin-dev-notice">
<p>
<?php echo esc_html( $message ); ?>
</p>
</div>
<?php
}
$extra_args = array(
'live_rates_types' => $this->service_schemas_store->get_all_shipping_method_ids(),
);
$carriers_response = $this->api_client->get_carrier_accounts();
if ( ! is_wp_error( $carriers_response ) && ! empty( $carriers_response->carriers ) ) {
$extra_args['carrier_accounts'] = $carriers_response->carriers;
}
// check the helper auth before calling wccom subscription api.
if ( ! is_wp_error( WC_Connect_Functions::get_wc_helper_auth_info() ) ) {
$subscriptions_usage_response = $this->api_client->get_wccom_subscriptions();
if ( ! is_wp_error( $subscriptions_usage_response ) && ! empty( $subscriptions_usage_response->subscriptions ) ) {
$extra_args['subscriptions'] = $subscriptions_usage_response->subscriptions;
}
}
if ( isset( $_GET['from_order'] ) ) {
$from_order = sanitize_text_field( $_GET['from_order'] );
$extra_args['order_id'] = $from_order;
$extra_args['order_href'] = get_edit_post_link( $from_order );
}
if ( ! empty( $_GET['carrier'] ) ) {
$carrier = sanitize_text_field( $_GET['carrier'] );
$extra_args['carrier'] = $carrier;
$extra_args['continents'] = $this->continents->get();
$carrier_information = array();
if ( ! empty( $extra_args['carrier_accounts'] ) ) {
$carrier_information = current(
array_filter(
$extra_args['carrier_accounts'],
function ( $carrier ) {
return $carrier->type === $carrier;
}
)
);
}
if ( ! empty( $carrier_information ) ) {
?>
<h2>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=woocommerce-services-settings' ) ); ?>"><?php esc_html_e( 'WooCommerce Shipping & Tax', 'woocommerce-services' ); ?></a> &gt;
<span><?php echo esc_html( $carrier_information->carrier ); ?></span>
</h2>
<?php
}
}
do_action( 'enqueue_wc_connect_script', 'wc-connect-shipping-settings', $extra_args );
}
}
}
@@ -0,0 +1,613 @@
<?php
if ( ! class_exists( 'WC_Connect_Shipping_Label' ) ) {
class WC_Connect_Shipping_Label {
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
/**
* @var WC_Connect_Account_Settings
*/
protected $account_settings;
/**
* @var WC_Connect_Package_Settings
*/
protected $package_settings;
/**
* @var WC_Connect_Continents
*/
protected $continents;
/**
* @var array Supported countries by USPS, see: https://webpmt.usps.gov/pmt010.cfm
*/
private $supported_countries = array( 'US', 'AS', 'PR', 'VI', 'GU', 'MP', 'UM', 'FM', 'MH' );
/**
* @var array Supported currencies
*/
private $supported_currencies = array( 'USD' );
private $show_metabox = null;
public function __construct(
WC_Connect_API_Client $api_client,
WC_Connect_Service_Settings_Store $settings_store,
WC_Connect_Service_Schemas_Store $service_schemas_store,
WC_Connect_Payment_Methods_Store $payment_methods_store
) {
$this->api_client = $api_client;
$this->settings_store = $settings_store;
$this->service_schemas_store = $service_schemas_store;
$this->account_settings = new WC_Connect_Account_Settings(
$settings_store,
$payment_methods_store
);
$this->package_settings = new WC_Connect_Package_Settings(
$settings_store,
$service_schemas_store
);
$this->continents = new WC_Connect_Continents();
}
public function get_item_data( WC_Order $order, $item ) {
$product = WC_Connect_Utils::get_item_product( $order, $item );
if ( ! $product || ! $product->needs_shipping() ) {
return null;
}
$height = 0;
$length = 0;
$weight = $product->get_weight();
$width = 0;
if ( $product->has_dimensions() ) {
$height = $product->get_height();
$length = $product->get_length();
$width = $product->get_width();
}
$parent_product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
$product_data = array(
'height' => (float) $height,
'product_id' => $product->get_id(),
'length' => (float) $length,
'quantity' => 1,
'weight' => (float) $weight,
'width' => (float) $width,
'name' => $this->get_name( $product ),
'url' => get_edit_post_link( $parent_product_id, null ),
);
if ( $product->is_type( 'variation' ) ) {
$product_data['attributes'] = wc_get_formatted_variation( $product, true );
}
return $product_data;
}
protected function get_packaging_from_shipping_method( $shipping_method ) {
if ( ! $shipping_method || ! isset( $shipping_method['wc_connect_packages'] ) ) {
return array();
}
$packages_data = $shipping_method['wc_connect_packages'];
if ( ! $packages_data ) {
return array();
}
// WC3 retrieves metadata as non-scalar values.
if ( is_array( $packages_data ) ) {
return $packages_data;
}
// WC2.6 stores non-scalar values as string, but doesn't deserialize it on retrieval.
$packages = maybe_unserialize( $packages_data );
if ( is_array( $packages ) ) {
return $packages;
}
// legacy WCS stored the labels as JSON.
$packages = json_decode( $packages_data, true );
if ( $packages ) {
return $packages;
}
$packages_data = $this->settings_store->try_recover_invalid_json_string( 'box_id', $packages_data );
$packages = json_decode( $packages_data, true );
if ( $packages ) {
return $packages;
}
return array();
}
protected function get_packaging_metadata( WC_Order $order ) {
$shipping_methods = $order->get_shipping_methods();
$shipping_method = reset( $shipping_methods );
$packaging = $this->get_packaging_from_shipping_method( $shipping_method );
if ( is_array( $packaging ) ) {
return array_filter( $packaging );
}
return array();
}
protected function get_name( WC_Product $product ) {
if ( $product->get_sku() ) {
$identifier = $product->get_sku();
} else {
$identifier = '#' . $product->get_id();
}
return sprintf( '%s - %s', $identifier, $product->get_title() );
}
public function get_selected_packages( WC_Order $order ) {
$packages = $this->get_packaging_metadata( $order );
if ( ! $packages ) {
$items = $this->get_all_items( $order );
$weight = array_sum( wp_list_pluck( $items, 'weight' ) );
$packages = array(
'default_box' => array(
'id' => 'default_box',
'box_id' => 'not_selected',
'height' => 0,
'length' => 0,
'weight' => $weight,
'width' => 0,
'items' => $items,
),
);
}
$formatted_packages = array();
foreach ( $packages as $package_obj ) {
$package = (array) $package_obj;
$package_id = $package['id'];
$formatted_packages[ $package_id ] = $package;
foreach ( $package['items'] as $item_index => $item ) {
$product_data = (array) $item;
$product = WC_Connect_Utils::get_item_product( $order, $product_data );
if ( $product ) {
$parent_product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
$product_data['name'] = $this->get_name( $product );
$product_data['url'] = get_edit_post_link( $parent_product_id, null );
if ( $product->is_type( 'variation' ) ) {
$product_data['attributes'] = wc_get_formatted_variation( $product, true );
}
$customs_info = $product->get_meta( 'wc_connect_customs_info', true );
if ( is_array( $customs_info ) ) {
$product_data = array_merge( $product_data, $customs_info );
}
} else {
$product_data['name'] = WC_Connect_Utils::get_product_name_from_order( $product_data['product_id'], $order );
}
$product_data['value'] = WC_Connect_Utils::get_product_price_from_order( $product_data['product_id'], $order );
if ( ! isset( $product_data['value'] ) ) {
$product_data['value'] = 0;
}
$formatted_packages[ $package_id ]['items'][ $item_index ] = $product_data;
}
}
return $formatted_packages;
}
public function get_all_items( WC_Order $order ) {
if ( $this->get_packaging_metadata( $order ) ) {
return array();
}
$items = array();
foreach ( $order->get_items() as $item ) {
$item_data = $this->get_item_data( $order, $item );
if ( null === $item_data ) {
continue;
}
$refunded_qty = $order->get_qty_refunded_for_item( $item->get_id() );
$remaining_quantity = $item['qty'] - absint( $refunded_qty );
/**
* The threshold at which we will start batching items together.
* As an example, if a single order item has a quantity 60, which is more than the default threshold
* value of 20 we will start batching together.
*/
$threshold = apply_filters( 'wc_connect_shipment_item_quantity_threshold', 20 );
/**
* Max number of shipments allowed to be created for this item should quantity of this order items
* exceeds `wc_connect_shipment_item_quantity_threshold`
*/
$max_shipments = apply_filters( 'wc_connect_max_shipments_if_quantity_exceeds_threshold', 5 );
$weight_per_item = $item_data['weight'];
$should_cap_shipments = $remaining_quantity > $threshold;
if ( $should_cap_shipments ) {
$quantity_per_shipment = floor( $remaining_quantity / $max_shipments );
for ( $i = 0; $i < $max_shipments; $i ++ ) {
$remaining_quantity -= $quantity_per_shipment;
if( $remaining_quantity >= $quantity_per_shipment ) {
$item_data['quantity'] = $quantity_per_shipment;
} else {
$item_data['quantity'] = $quantity_per_shipment + $remaining_quantity;
}
$item_data['weight'] = round( $item_data['quantity'] * $weight_per_item, 2 );
$items[] = $item_data;
}
} else {
for ( $i = 0; $i < $remaining_quantity; $i ++ ) {
$items[] = $item_data;
}
}
}
return $items;
}
public function get_selected_rates( WC_Order $order ) {
$shipping_methods = $order->get_shipping_methods();
$shipping_method = reset( $shipping_methods );
$packages = $this->get_packaging_from_shipping_method( $shipping_method );
$rates = array();
foreach ( $packages as $idx => $package_obj ) {
$package = (array) $package_obj;
// Abort if the package data is malformed
if ( ! isset( $package['id'] ) || ! isset( $package['service_id'] ) ) {
return array();
}
$rates[ $package['id'] ] = $package['service_id'];
}
return $rates;
}
protected function format_address_for_api( $address ) {
// Combine first and last name.
if ( ! isset( $address['name'] ) ) {
$first_name = isset( $address['first_name'] ) ? trim( $address['first_name'] ) : '';
$last_name = isset( $address['last_name'] ) ? trim( $address['last_name'] ) : '';
$address['name'] = $first_name . ' ' . $last_name;
}
// Rename address_1 to address.
if ( ! isset( $address['address'] ) && isset( $address['address_1'] ) ) {
$address['address'] = $address['address_1'];
}
// Remove now defunct keys.
unset( $address['first_name'], $address['last_name'], $address['address_1'] );
return $address;
}
protected function get_origin_address() {
$origin = $this->format_address_for_api( $this->settings_store->get_origin_address() );
return $origin;
}
protected function get_destination_address( WC_Order $order ) {
$order_address = $order->get_address( 'shipping' );
$destination = $this->format_address_for_api( $order_address );
return $destination;
}
protected function get_form_data( WC_Order $order ) {
$order_id = $order->get_id();
$selected_packages = $this->get_selected_packages( $order );
$is_packed = ( false !== $this->get_packaging_metadata( $order ) );
$origin = $this->get_origin_address();
$selected_rates = $this->get_selected_rates( $order );
$destination = $this->get_destination_address( $order );
if ( ! $destination['country'] ) {
$destination['country'] = $origin['country'];
}
$origin_normalized = (bool) WC_Connect_Options::get_option( 'origin_address', false );
$destination_normalized = (bool) $order->get_meta( '_wc_connect_destination_normalized', true );
$form_data = compact( 'is_packed', 'selected_packages', 'origin', 'destination', 'origin_normalized', 'destination_normalized' );
$form_data['rates'] = array(
'selected' => (object) $selected_rates,
);
$form_data['order_id'] = $order_id;
return $form_data;
}
/**
* Check whether the given order is eligible for shipping label creation - the order has at least one product that is:
* - Shippable.
* - Non-refunded.
*
* @param WC_Order $order The order to check for shipping label creation eligibility.
* @return bool Whether the given order is eligible for shipping label creation.
*/
public function is_order_eligible_for_shipping_label_creation( WC_Order $order ) {
// Set up a dictionary from product ID to quantity in the order, which will be updated by refunds and existing labels later.
$quantities_by_product_id = array();
foreach ( $order->get_items() as $item ) {
$product = WC_Connect_Utils::get_item_product( $order, $item );
if ( $product && $product->needs_shipping() ) {
$product_id = $product->get_id();
$current_quantity = array_key_exists( $product_id, $quantities_by_product_id ) ? $quantities_by_product_id[ $product_id ] : 0;
$quantities_by_product_id[ $product_id ] = $current_quantity + $item->get_quantity();
}
}
// A shipping label cannot be created without a shippable product.
if ( empty( $quantities_by_product_id ) ) {
return false;
}
// Update the quantity for each refunded product ID in the order.
foreach ( $order->get_refunds() as $refund ) {
foreach ( $refund->get_items() as $refunded_item ) {
$product = WC_Connect_Utils::get_item_product( $order, $refunded_item );
if ( ! is_a( $product, 'WC_Product' ) ) {
continue;
}
$product_id = $product->get_id();
if ( array_key_exists( $product_id, $quantities_by_product_id ) ) {
$current_count = $quantities_by_product_id[ $product_id ];
$quantities_by_product_id[ $product_id ] = $current_count - abs( $refunded_item->get_quantity() );
}
}
}
// The order is eligible for shipping label creation when there is at least one product with positive quantity.
foreach ( $quantities_by_product_id as $product_id => $quantity ) {
if ( $quantity > 0 ) {
return true;
}
}
return false;
}
/**
* Check whether the store is eligible for shipping label creation:
* - Store currency is supported.
* - Store country is supported.
*
* @return bool Whether the WC store is eligible for shipping label creation.
*/
public function is_store_eligible_for_shipping_label_creation() {
$base_currency = get_woocommerce_currency();
if ( ! $this->is_supported_currency( $base_currency ) ) {
return false;
}
$base_location = wc_get_base_location();
if ( ! $this->is_supported_country( $base_location['country'] ) ) {
return false;
}
return true;
}
/**
* Check whether the given country code is supported for shipping labels.
*
* @param string $country_code Country code of the WC store.
* @return bool Whether the given country code is supported for shipping labels.
*/
private function is_supported_country( $country_code ) {
return in_array( $country_code, $this->supported_countries, true );
}
/**
* Check whether the given currency code is supported for shipping labels.
*
* @param string $currency_code Currency code of the WC store.
* @return bool Whether the given country code is supported for shipping labels.
*/
private function is_supported_currency( $currency_code ) {
return in_array( $currency_code, $this->supported_currencies, true );
}
public function is_dhl_express_available() {
$dhl_express = $this->service_schemas_store->get_service_schema_by_id( 'dhlexpress' );
return ! ! $dhl_express;
}
public function is_order_dhl_express_eligible() {
if ( ! $this->is_dhl_express_available() ) {
return false;
}
global $post;
$order = WC_Connect_Compatibility::instance()->init_theorder_object( $post );
if ( ! $order ) {
return false;
}
$origin = $this->get_origin_address();
$destination = $this->get_destination_address( $order );
return $origin['country'] !== $destination['country'];
}
/**
* Check if meta boxes should be displayed.
*
* @param WP_Post $post Post object.
* @return boolean
*/
public function should_show_meta_box( $post ) {
if ( null === $this->show_metabox ) {
$this->show_metabox = $this->calculate_should_show_meta_box( $post );
}
return $this->show_metabox;
}
/**
* Check if meta boxes should be displayed.
*
* @param WP_Post $post Post object.
* @return bool
*/
private function calculate_should_show_meta_box( $post ) {
// not all users have the permission to manage shipping labels.
// if a request is made to the JS backend and the user doesn't have permission, an error would be displayed.
if ( ! WC_Connect_Functions::user_can_manage_labels() ) {
return false;
}
$order = WC_Connect_Compatibility::instance()->init_theorder_object( $post );
if ( ! $order ) {
return false;
}
// If the shipping label is disabled, will remove the meta box.
if ( ! $this->is_shipping_label_enabled() ) {
return false;
}
// If the order already has purchased labels, show the meta-box no matter what.
if ( $order->get_meta( 'wc_connect_labels', true ) ) {
return true;
}
// Restrict showing the metabox to supported store countries and currencies.
if ( ! $this->is_store_eligible_for_shipping_label_creation() ) {
return false;
}
// If the order was created using WCS checkout rates, show the meta-box regardless of the products' state.
if ( $this->get_packaging_metadata( $order ) ) {
return true;
}
// At this point (no packaging data), only show if there's at least one existing and shippable product.
foreach ( $order->get_items() as $item ) {
$product = WC_Connect_Utils::get_item_product( $order, $item );
if ( $product && $product->needs_shipping() ) {
return true;
}
}
return false;
}
/**
* Check whether shipping label feature is enabled from WC Services setting.
*
* @return bool True if shipping label is enabled from the settings.
*/
public function is_shipping_label_enabled() {
$account_settings = $this->account_settings->get();
if ( isset( $account_settings['formData']['enabled'] ) && is_bool( $account_settings['formData']['enabled'] ) ) {
return $account_settings['formData']['enabled'];
}
return true;
}
public function get_label_payload( $post_order_or_id ) {
$order = wc_get_order( $post_order_or_id );
if ( ! is_a( $order, 'WC_Order' ) ) {
return false;
}
$order_id = $order->get_id();
$payload = array(
'orderId' => $order_id,
'paperSize' => $this->settings_store->get_preferred_paper_size(),
'formData' => $this->get_form_data( $order ),
'labelsData' => $this->settings_store->get_label_order_meta_data( $order_id ),
'storeOptions' => $this->settings_store->get_store_options(),
// for backwards compatibility, still disable the country dropdown for calypso users with older plugin versions.
'canChangeCountries' => true,
);
return $payload;
}
/**
* Filter items needing shipping callback.
*
* @since 3.0.0
* @param array $item Item to check for shipping.
* @return bool
*/
public function filter_items_needing_shipping( $item ) {
$product = $item->get_product();
return $product && $product->needs_shipping();
}
/**
* Reduce items to sum their quantities.
*
* @param int $sum Current sum.
* @param array $item Item to add to sum.
* @return int
*/
protected function reducer_items_quantity( $sum, $item ) {
return $sum + $item->get_quantity();
}
public function meta_box( $post, $args ) {
$connect_order_presenter = new WC_Connect_Order_Presenter();
$order = WC_Connect_Compatibility::instance()->init_theorder_object( $post );
$items = array_filter( $order->get_items(), array( $this, 'filter_items_needing_shipping' ) );
$items_count = array_reduce( $items, array( $this, 'reducer_items_quantity' ), 0 ) - absint( $order->get_item_count_refunded() );
$payload = apply_filters(
'wc_connect_meta_box_payload',
array(
'order' => $connect_order_presenter->get_order_for_api( $order ),
'accountSettings' => $this->account_settings->get(),
'packagesSettings' => $this->package_settings->get(),
'shippingLabelData' => $this->get_label_payload( $order->get_id() ),
'continents' => $this->continents->get(),
'euCountries' => WC()->countries->get_european_union_countries(),
'context' => $args['args']['context'],
'items' => $items_count,
),
$args,
$order,
$this
);
do_action( 'enqueue_wc_connect_script', 'wc-connect-create-shipping-label', $payload );
}
}
}
@@ -0,0 +1,744 @@
<?php
if ( ! class_exists( 'WC_Connect_Shipping_Method' ) ) {
class WC_Connect_Shipping_Method extends WC_Shipping_Method {
/**
* @var object A reference to a the fetched properties of the service
*/
protected $service_schema = null;
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $service_settings_store;
/**
* @var WC_Connect_Logger
*/
protected $logger;
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* Store validation errors in property for later retrieval.
*
* @var WP_Error
*/
protected $package_validation_errors;
/**
* Cache of destinations which have already been validated.
*
* @var array
*/
protected $validated_package_destinations = array();
public function __construct( $id_or_instance_id = null ) {
parent::__construct( $id_or_instance_id );
// If $arg looks like a number, treat it as an instance_id,
// otherwise, treat it as a (method) id (e.g. wc_connect_usps).
if ( is_numeric( $id_or_instance_id ) ) {
$this->instance_id = absint( $id_or_instance_id );
} else {
$this->instance_id = null;
}
/**
* Provide a dependency injection point for each shipping method.
*
* WooCommerce core instantiates shipping method with only a string ID
* or a numeric instance ID. We depend on more than that, so we need
* to provide a hook for our plugin to inject dependencies into each
* shipping method instance.
*
* @param WC_Connect_Shipping_Method $this
* @param int|string $id_or_instance_id
*/
do_action( 'wc_connect_service_init', $this, $id_or_instance_id );
if ( ! $this->service_schema ) {
$this->log_error(
'Error. A WC_Connect_Shipping_Method was constructed without an id or instance_id',
__FUNCTION__
);
$this->id = 'wc_connect_uninitialized_shipping_method';
$this->method_title = '';
$this->method_description = '';
$this->supports = array();
$this->title = '';
} else {
$this->id = $this->service_schema->method_id;
$this->method_title = $this->service_schema->method_title;
$this->method_description = $this->service_schema->method_description;
$this->supports = array(
'shipping-zones',
'instance-settings',
);
// Set title to default value.
$this->title = $this->service_schema->method_title;
// Load form values from options, updating title if present.
$this->init_form_settings();
// Note - we cannot hook admin_enqueue_scripts here because we need an instance id
// and this constructor is not called with an instance id until after
// admin_enqueue_scripts has already fired. This is why WC_Connect_Loader
// does it instead.
}
$this->package_validation_errors = new WP_Error();
}
public function get_service_schema() {
return $this->service_schema;
}
public function set_service_schema( $service_schema ) {
$this->service_schema = $service_schema;
}
public function get_service_settings_store() {
return $this->service_settings_store;
}
public function set_service_settings_store( $service_settings_store ) {
$this->service_settings_store = $service_settings_store;
}
public function get_logger() {
return $this->logger;
}
public function set_logger( WC_Connect_Logger $logger ) {
$this->logger = $logger;
}
public function get_api_client() {
return $this->api_client;
}
public function set_api_client( WC_Connect_API_Client $api_client ) {
$this->api_client = $api_client;
}
/**
* Logging helper.
*
* Avoids calling methods on an undefined object if no logger was
* injected during the init action in the constructor.
*
* @see WC_Connect_Logger::debug()
* @param string|WP_Error $message
* @param string $context
*/
protected function log( $message, $context = '' ) {
$logger = $this->get_logger();
if ( is_a( $logger, 'WC_Connect_Logger' ) ) {
$logger->log( $message, $context );
}
}
protected function log_error( $message, $context = '' ) {
$logger = $this->get_logger();
if ( is_a( $logger, 'WC_Connect_Logger' ) ) {
$logger->error( $message, $context );
}
}
/**
* Restores any values persisted to the DB for this service instance
* and sets up title for WC core to work properly
*/
protected function init_form_settings() {
$form_settings = $this->get_service_settings();
// We need to initialize the instance title ($this->title)
// from the settings blob.
if ( property_exists( $form_settings, 'title' ) ) {
$this->title = $form_settings->title;
}
}
/**
* Returns the settings for this service (e.g. for use in the form or for
* sending to the rate request endpoint
*
* Used by WC_Connect_Loader to embed the form schema in the page for JS to consume
*
* @return object
*/
public function get_service_settings() {
$service_settings = $this->service_settings_store->get_service_settings( $this->id, $this->instance_id );
if ( ! is_object( $service_settings ) ) {
$service_settings = new stdClass();
}
if ( ! property_exists( $service_settings, 'services' ) ) {
return $service_settings;
}
return $service_settings;
}
/**
* Determine if a package's destination is valid enough for a rate quote.
*
* @param array $package Current Package.
* @return bool
*/
public function is_valid_package_destination( $package ) {
$country = isset( $package['destination']['country'] ) ? $package['destination']['country'] : '';
$postcode = isset( $package['destination']['postcode'] ) ? $package['destination']['postcode'] : '';
$state = isset( $package['destination']['state'] ) ? $package['destination']['state'] : '';
$countries = WC()->countries->get_countries();
$destination_key = md5( wp_json_encode( $package['destination'] ) );
if ( isset( $this->validated_package_destinations[ $destination_key ] ) ) {
// We are using a cache because this method could be called multiple times and we don't want to show double errors.
return $this->validated_package_destinations[ $destination_key ];
}
// Ensure that Country is specified.
if ( empty( $country ) ) {
$this->package_validation_errors->add(
'country_required',
esc_html__( 'A country is required', 'woocommerce-services' ),
[ 'id' => 'country' ]
);
}
// Validate Postcode.
if ( ! WC_Validation::is_postcode( $postcode, $country ) ) {
$fields = WC()->countries->get_address_fields( $country, '' );
if ( empty( $postcode ) ) {
$this->package_validation_errors->add(
'postcode_required',
sprintf(
/* Translators: %1$s: Localized label for Zip/postal code, %2$s: Country name */
esc_html__(
'A %1$s is required for %2$s.',
'woocommerce-services'
),
'<strong>' . esc_html( $fields['postcode']['label'] ) . '</strong>',
'<strong>' . esc_html( $countries[ $country ] ) . '</strong>'
),
[ 'id' => 'postcode' ]
);
} else {
$this->package_validation_errors->add(
'postcode_validation',
sprintf(
/* Translators: %1$s: Localized label for Zip/postal code, %2$s: submitted zip/postal code, %3$s: Country name */
esc_html__(
'%1$s %2$s is invalid for %3$s.',
'woocommerce-services'
),
esc_html( $fields['postcode']['label'] ),
'<strong>' . esc_html( $postcode ) . '</strong>',
'<strong>' . esc_html( $countries[ $country ] ) . '</strong>'
),
[ 'id' => 'postcode' ]
);
}
}
// Validate State.
$valid_states = WC()->countries->get_states( $country );
if ( $valid_states && ! isset( $valid_states[ $state ] ) ) {
if ( empty( $state ) ) {
$fields = WC()->countries->get_address_fields( $country, '' );
$this->package_validation_errors->add(
'state_required',
sprintf(
/* Translators: %1$s: Localized label for province/region/state, %2$s: Country name */
esc_html__(
'A %1$s is required for %2$s.',
'woocommerce-services'
),
'<strong>' . esc_html( $fields['state']['label'] ) . '</strong>',
'<strong>' . esc_html( $countries[ $country ] ) . '</strong>'
),
[ 'id' => 'state' ]
);
} else {
$this->package_validation_errors->add(
'state_validation',
sprintf(
/* Translators: %1$s: State name, %2$s: Country name */
esc_html__(
'State %1$s is invalid for %2$s.',
'woocommerce-services'
),
'<strong>' . esc_html( $state ) . '</strong>',
'<strong>' . esc_html( $countries[ $country ] ) . '</strong>'
),
[ 'id' => 'state' ]
);
}
}
$is_valid = ! $this->package_validation_errors->has_errors();
$this->validated_package_destinations[ $destination_key ] = $is_valid;
return $is_valid;
}
/**
* Return WP_Error object which may have validation errors.
*
* @return WP_Error
*/
public function get_package_validation_errors() {
return $this->package_validation_errors;
}
private function lookup_product( $package, $product_id ) {
foreach ( $package['contents'] as $item ) {
if ( $item['product_id'] === $product_id || $item['variation_id'] === $product_id ) {
return $item['data'];
}
}
return false;
}
private function filter_preset_boxes( $preset_id ) {
return is_string( $preset_id );
}
private function check_and_handle_response_error( $response_body, $service_settings ) {
if ( is_wp_error( $response_body ) ) {
$this->debug(
sprintf(
'Request failed: %s',
$response_body->get_error_message()
),
'error'
);
$this->log_error(
sprintf(
'Error. Unable to get shipping rate(s) for %s instance id %d.',
$this->id,
$this->instance_id
),
__FUNCTION__
);
$this->set_last_request_failed();
$this->log_error( $response_body, __FUNCTION__ );
$this->add_fallback_rate( $service_settings );
return true;
}
if ( ! property_exists( $response_body, 'rates' ) ) {
$this->debug( 'Response is missing `rates` property', 'error' );
$this->set_last_request_failed();
$this->add_fallback_rate( $service_settings );
return true;
}
return false;
}
private function add_fallback_rate( $service_settings ) {
if ( ! property_exists( $service_settings, 'fallback_rate' ) || 0 >= $service_settings->fallback_rate ) {
return;
}
$this->debug( 'No rates found, adding fallback.', 'error' );
$rate_to_add = array(
'id' => self::format_rate_id( 'fallback', $this->id, 0 ),
'label' => self::format_rate_title( $this->service_schema->carrier_name ),
'cost' => $service_settings->fallback_rate,
);
$this->add_rate( $rate_to_add );
}
public function calculate_shipping( $package = array() ) {
if ( ! WC_Connect_Functions::should_send_cart_api_request() ) {
return;
}
$this->debug(
sprintf(
'WooCommerce Shipping & Tax debug mode is on - to hide these messages, turn debug mode off in the <a href="%s" style="text-decoration: underline;">settings</a>.',
admin_url( 'admin.php?page=wc-status&tab=connect' )
)
);
if ( ! $this->is_valid_package_destination( $package ) ) {
if ( WC_Connect_functions::is_cart() || WC_Connect_functions::is_checkout() || WC_Connect_functions::is_store_api_call() ) {
foreach ( $this->package_validation_errors->errors as $code => $messages ) {
foreach ( $messages as $message ) {
// Using debug instead of regular notice because the error always shows before customer enters any shipping information.
$this->debug( $message, 'error' );
}
}
}
return;
}
$service_settings = $this->get_service_settings();
$settings_keys = get_object_vars( $service_settings );
if ( empty( $settings_keys ) ) {
$this->log(
sprintf(
'Service settings empty. Skipping %s rate request (instance id %d).',
$this->id,
$this->instance_id
),
__FUNCTION__
);
return;
}
// TODO: Request rates for all WooCommerce Shipping & Tax powered methods in
// the current shipping zone to avoid each method making an independent request.
$services = array(
array(
'id' => $this->service_schema->id,
'instance' => $this->instance_id,
'service_settings' => $service_settings,
),
);
$custom_boxes = $this->service_settings_store->get_packages();
$predefined_boxes = $this->service_settings_store->get_predefined_packages_for_service( $this->service_schema->id );
$predefined_boxes = array_values( array_filter( $predefined_boxes, array( $this, 'filter_preset_boxes' ) ) );
$cache_key = sprintf(
'wcs_rates_%s',
md5( serialize( array( $services, $package, $custom_boxes, $predefined_boxes ) ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
);
$is_debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' );
$response_body = get_transient( $cache_key );
$this->debug( false === $response_body ? 'Cache does not contain rates response' : 'Cache contains rates response' );
if ( ! $is_debug_mode && false !== $response_body ) {
$this->debug( 'Rates response retrieved from cache' );
} else {
$response_body = $this->api_client->get_shipping_rates( $services, $package, $custom_boxes, $predefined_boxes );
if ( $this->check_and_handle_response_error( $response_body, $service_settings ) ) {
return;
}
set_transient( $cache_key, $response_body, HOUR_IN_SECONDS );
}
$instances = $response_body->rates;
foreach ( (array) $instances as $instance ) {
if ( property_exists( $instance, 'error' ) ) {
$this->log_error( $instance->error, __FUNCTION__ );
$this->set_last_request_failed();
}
if ( ! property_exists( $instance, 'rates' ) ) {
continue;
}
$packaging_lookup = $this->service_settings_store->get_package_lookup();
foreach ( (array) $instance->rates as $rate_idx => $rate ) {
$package_summaries = array();
$service_ids = array();
$dimension_unit = get_option( 'woocommerce_dimension_unit' );
$weight_unit = get_option( 'woocommerce_weight_unit' );
$measurements_format = '(%s x %s x %s ' . $dimension_unit . ', %s ' . $weight_unit . ')';
foreach ( $rate->packages as $rate_package ) {
$service_ids[] = $rate_package->service_id;
$item_product_ids = array();
$item_by_product = array();
foreach ( $rate_package->items as $package_item ) {
$item_product_ids[] = $package_item->product_id;
$item_by_product[ $package_item->product_id ] = $package_item;
}
$product_summaries = array();
$product_counts = array_count_values( $item_product_ids );
foreach ( $product_counts as $product_id => $count ) {
/**
* WC Product.
*
* @var WC_product $product
*/
$product = $this->lookup_product( $package, $product_id );
if ( is_a( $product, 'WC_Product' ) ) {
$item_name = $product->get_name();
$item = $item_by_product[ $product_id ];
$item_measurements = sprintf( $measurements_format, $item->length, $item->width, $item->height, $item->weight );
$product_summaries[] =
( $count > 1 ? sprintf( '<em>%d x</em> ', $count ) : '' ) .
sprintf( '(ID: %d) <strong>%s</strong> %s', $product_id, esc_html( $item_name ), esc_html( $item_measurements ) );
}
}
$package_measurements = '';
if ( ! property_exists( $rate_package, 'box_id' ) ) {
$package_name = __( 'Unknown package', 'woocommerce-services' );
} elseif ( 'individual' === $rate_package->box_id ) {
$package_name = __( 'Individual packaging', 'woocommerce-services' );
} elseif (
isset( $packaging_lookup[ $rate_package->box_id ] ) &&
isset( $packaging_lookup[ $rate_package->box_id ]['name'] )
) {
$package_name = $packaging_lookup[ $rate_package->box_id ]['name'];
$package_measurements = sprintf(
$measurements_format,
$rate_package->length,
$rate_package->width,
$rate_package->height,
$rate_package->weight
);
}
$package_summaries[] = sprintf( '<strong>%s</strong> %s', $package_name, $package_measurements )
. '<ul><li>' . implode( '</li><li>', $product_summaries ) . '</li></ul>';
}
$packaging_info = implode( ', ', $package_summaries );
$services_list = implode( '-', array_unique( $service_ids ) );
$box_packing_log = empty( $rate->box_packing_log ) ? array() : $rate->box_packing_log;
$rate_to_add = array(
// Make sure the rate ID is identifiable for extensions like Conditional Shipping and Payments.
// The new format looks like: `wc_services_usps:1:pri_medium_flat_box_top`.
'id' => self::format_rate_id( $this->id, $instance->instance, $services_list ),
'label' => self::format_rate_title( $rate->title ),
'cost' => $rate->rate,
'meta_data' => array(
'wc_connect_packages' => $rate->packages,
__( 'Packaging', 'woocommerce-services' ) => $packaging_info,
'wc_connect_packing_log' => $box_packing_log,
),
);
if ( $this->logger->is_debug_enabled() ) {
if ( 'fallback' === $services_list ) {
// Notify the merchant when the fallback rate is added by the WCS server.
$this->debug( 'No rates found, adding fallback.', 'error' );
} else {
$rate_debug = '<strong>';
$rate_debug .= sprintf(
/* translators: 1: name of shipping service, 2: shipping rate (price) */
__( 'Received rate: %1$s (%2$s)', 'woocommerce-services' ),
$rate_to_add['label'],
wc_price( $rate->rate )
);
$rate_debug .= '</strong><ul><li>' . implode( '</li><li>', $package_summaries ) . '</li></ul>';
if ( ! empty( $box_packing_log ) ) {
$rate_debug .= '<strong>' . __( 'Packing log:', 'woocommerce-services' ) . '</strong>';
$rate_debug .= '<ul><li>' . implode( '</li><li>', array_map( 'esc_html', $box_packing_log ) ) . '</li></ul>';
}
$this->debug( $rate_debug, 'success' );
}
}
$this->add_rate( $rate_to_add );
}
}
if ( 0 === count( $this->rates ) ) {
$this->add_fallback_rate( $service_settings );
} else {
$this->set_last_request_failed( 0 );
}
$this->update_last_rate_request_timestamp();
}
public function update_last_rate_request_timestamp() {
$previous_timestamp = WC_Connect_Options::get_option( 'last_rate_request' );
if ( false === $previous_timestamp ||
( time() - HOUR_IN_SECONDS ) > $previous_timestamp ) {
WC_Connect_Options::update_option( 'last_rate_request', time() );
}
}
public function set_last_request_failed( $timestamp = null ) {
if ( is_null( $timestamp ) ) {
$timestamp = time();
}
WC_Connect_Options::update_shipping_method_option( 'failure_timestamp', $timestamp, $this->id, $this->instance_id );
}
public function admin_options() {
// hide WP native save button on settings page.
global $hide_save_button;
$hide_save_button = true;
do_action( 'wc_connect_service_admin_options', $this->id, $this->instance_id );
}
/**
* @param string $method_id
* @param int $instance_id
* @param string $service_ids
*
* @return string
*/
public static function format_rate_id( $method_id, $instance_id, $service_ids ) {
return sprintf( '%s:%d:%s', $method_id, $instance_id, $service_ids );
}
public static function format_rate_title( $rate_title ) {
$formatted_title = wp_kses(
html_entity_decode( $rate_title ),
array(
'sup' => array(),
'del' => array(),
'small' => array(),
'em' => array(),
'i' => array(),
'strong' => array(),
)
);
return $formatted_title;
}
/**
* Log debug by printing it as notice.
*
* @param string $message Debug message.
* @param string $type Notice type.
*/
public function debug( $message, $type = 'notice' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing --- No input from $_POST is used as input.
if ( WC_Connect_Functions::is_cart() || WC_Connect_Functions::is_checkout() || isset( $_POST['update_cart'] ) || WC_Connect_Functions::is_store_api_call() ) {
$debug_message = sprintf( '%s (%s:%d)', $message, esc_html( $this->title ), $this->instance_id );
$this->logger->debug( $debug_message, $type );
$this->logger->log( $debug_message, __CLASS__ );
}
}
/**
* Is this method available?
*
* @param array $package Package.
* @return bool
*/
public function is_available( $package ) {
if ( ! parent::is_available( $package ) ) {
return false;
}
if ( ! $this->matches_package_shipping_classes( $package ) ) {
return false;
}
return true;
}
/**
* Checks whether the shipping classes of all products in a package are
* actually supported by the method. If a single product has an un-supported class,
* the whole package will not be supported by the method.
*
* @param array $package The contents of a package.
* @return bool
*/
public function matches_package_shipping_classes( $package ) {
$settings = $this->get_service_settings();
$method_classes = property_exists( $settings, 'shipping_classes' )
? $settings->shipping_classes
: array();
// No checks needed if the method is not limited to certain classes.
if ( empty( $method_classes ) ) {
return true;
}
// Go through the cart contents and check if all products are supported.
foreach ( $package['contents'] as $item ) {
$shipping_class_id = $item['data']->get_shipping_class_id();
if ( in_array( $shipping_class_id, $method_classes, true ) ) {
continue;
}
if ( ! $this->logger->is_debug_enabled() ) {
return false;
}
$message = 'Skipping the "%1$s" shipping method because %2$s (%3$s) does not match the shipping classes specified in the method settings (%4$s).';
$product_class_name = 'No shipping class';
if ( $shipping_class_id ) {
$shipping_class = get_term_by( 'id', $shipping_class_id, 'product_shipping_class' );
if ( $shipping_class ) {
$product_class_name = $shipping_class->name;
}
}
$method_classes = get_terms(
array(
'taxonomy' => 'product_shipping_class',
'hide_empty' => false,
'include' => $method_classes,
)
);
if ( ! is_wp_error( $method_classes ) && ! empty( $method_classes ) ) {
$class_names = implode( ', ', wp_list_pluck( $method_classes, 'name' ) );
} else {
$class_names = 'No shipping classes found';
}
$message = sprintf(
$message,
$this->title,
$item['data']->get_title(),
$product_class_name,
$class_names
);
$this->debug( $message );
return false;
}
return true;
}
}
}
@@ -0,0 +1,118 @@
<?php
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Tracks' ) ) {
class WC_Connect_Tracks {
static $product_name = 'woocommerceconnect';
/**
* @var WC_Connect_Logger
*/
protected $logger;
/**
* Plugin file path.
*
* @var string
*/
public $plugin_file;
public function __construct( WC_Connect_Logger $logger, $plugin_file ) {
$this->logger = $logger;
$this->plugin_file = $plugin_file;
}
public function init() {
add_action( 'wc_connect_shipping_zone_method_added', array( $this, 'shipping_zone_method_added' ), 10, 3 );
add_action( 'wc_connect_shipping_zone_method_deleted', array( $this, 'shipping_zone_method_deleted' ), 10, 3 );
add_action( 'wc_connect_shipping_zone_method_status_toggled', array( $this, 'shipping_zone_method_status_toggled' ), 10, 4 );
add_action( 'wc_connect_saved_service_settings', array( $this, 'saved_service_settings' ), 10, 3 );
register_deactivation_hook( $this->plugin_file, array( $this, 'opted_out' ) );
}
public function opted_in( $source = null ) {
if ( is_null( $source ) ) {
$this->record_user_event( 'opted_in' );
} else {
$this->record_user_event( 'opted_in', compact( 'source' ) );
}
}
public function opted_out() {
$this->record_user_event( 'opted_out' );
}
public function shipping_zone_method_added( $instance_id, $service_id ) {
$this->record_user_event( 'shipping_zone_method_added' );
$this->record_user_event( 'shipping_zone_' . $service_id . '_added' );
}
public function shipping_zone_method_deleted( $instance_id, $service_id ) {
$this->record_user_event( 'shipping_zone_method_deleted' );
$this->record_user_event( 'shipping_zone_' . $service_id . '_deleted' );
}
public function shipping_zone_method_status_toggled( $instance_id, $service_id, $zone_id, $enabled ) {
if ( $enabled ) {
$this->record_user_event( 'shipping_zone_method_enabled' );
$this->record_user_event( 'shipping_zone_' . $service_id . '_enabled' );
} else {
$this->record_user_event( 'shipping_zone_method_disabled' );
$this->record_user_event( 'shipping_zone_' . $service_id . '_disabled' );
}
}
public function saved_service_settings( $service_id ) {
$this->record_user_event( 'saved_service_settings' );
$this->record_user_event( 'saved_' . $service_id . '_settings' );
}
public function record_user_event( $event_type, $data = array() ) {
$user = wp_get_current_user();
// Check for WooCommerce
$wc_version = 'unavailable';
if ( function_exists( 'WC' ) ) {
$wc_version = WC()->version;
}
$jetpack_blog_id = WC_Connect_Jetpack::get_wpcom_site_id();
if ( $jetpack_blog_id instanceof WP_Error ) {
$jetpack_blog_id = -1;
}
if ( ! is_array( $data ) ) {
$data = array();
}
$data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
$data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
$data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
$data['blog_url'] = get_option( 'siteurl' );
$data['blog_id'] = $jetpack_blog_id;
$data['wcs_version'] = WC_Connect_Loader::get_wcs_version();
$data['jetpack_version'] = 'embed-' . WC_Connect_Jetpack::get_jetpack_connection_package_version();
$data['is_atomic'] = WC_Connect_Jetpack::is_atomic_site();
$data['wc_version'] = $wc_version;
$data['wp_version'] = get_bloginfo( 'version' );
$event_type = self::$product_name . '_' . $event_type;
$this->debug( 'Tracked the following event: ' . $event_type );
WC_Connect_Jetpack::tracks_record_event( $user, $event_type, $data );
}
protected function debug( $message ) {
if ( ! is_null( $this->logger ) ) {
$this->logger->log( $message );
}
}
}
}
@@ -0,0 +1,108 @@
<?php
/**
* A class for working around the quirks and different versions of WordPress/WooCommerce
* This is for versions higher than 2.6 (3.0 and higher)
*/
// No direct access please.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Utils' ) ) {
/**
* WC_Connect_Compatibility class.
*/
class WC_Connect_Utils {
/**
* For a given product ID, it tries to find its name inside an order's line items.
* This is useful when an order has a product which was later deleted from the
* store.
*
* @param int $product_id Product ID or variation ID.
* @param WC_Order $order WC Order.
*
* @return string The product (or variation) name, ready to print
*/
public static function get_product_name_from_order( $product_id, $order ) {
$line_item = self::get_line_item_from_order( $product_id, $order );
if ( ! $line_item ) {
/* translators: %d: Deleted Product ID */
return sprintf( __( '#%d - [Deleted product]', 'woocommerce-services' ), $product_id );
}
/* translators: %1$d: Product ID, %2$s: Product Name */
return sprintf( __( '#%1$d - %2$s', 'woocommerce-services' ), $product_id, $line_item->get_name() );
}
/**
* For a given product ID, it tries to find its price inside an order's line items.
*
* @param int $product_id Product ID or variation ID.
* @param WC_Order $order WC Order.
*
* @return float The product (or variation) price, or NULL if it wasn't found
*/
public static function get_product_price_from_order( $product_id, $order ) {
$line_item = self::get_line_item_from_order( $product_id, $order );
if ( ! $line_item ) {
return null;
}
return round( floatval( $line_item->get_total() ) / $line_item->get_quantity(), 2 );
}
/**
* Retrieve the corresponding Product for the given Order Item.
*
* @param WC_Order $order WC Order.
* @param WC_Order_Item|WC_Order_Item_Product|array $item Order Item.
*
* @return WC_Product|null|false
*/
public static function get_item_product( WC_Order $order, $item ) {
if ( is_array( $item ) && isset( $item['product_id'] ) ) {
return wc_get_product( $item['product_id'] );
}
if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
/**
* Order Item Product
*
* @var WC_Order_Item_Product $item
*/
return $item->get_product();
}
return false;
}
/**
* Check if order contains given product.
*
* @param int $product_id WC Product ID.
* @param WC_Order $order WC Order.
*
* @return WC_Order_Item_Product|false
*/
public static function get_line_item_from_order( $product_id, $order ) {
/**
* Order Item Product
*
* @var WC_Order_Item_Product $line_item
*/
foreach ( $order->get_items() as $line_item ) {
$line_product_id = $line_item->get_product_id();
$line_variation_id = $line_item->get_variation_id();
if ( $line_product_id === $product_id || $line_variation_id === $product_id ) {
return $line_item;
}
}
return false;
}
}
}
@@ -0,0 +1,37 @@
<?php
/**
* Enum for WCS&T to WCShipping migration states.
*/
class WC_Connect_WCST_To_WCShipping_Migration_State_Enum {
// These are used for WCS&T to WCShipping migration.
public const NOT_STARTED = 1;
public const STARTED = 2;
public const ERROR_STARTED = 3;
public const INSTALLING = 4;
public const ERROR_INSTALLING = 5;
public const ACTIVATING = 6;
public const ERROR_ACTIVATING = 7;
public const DB_MIGRATION = 8;
public const ERROR_DB_MIGRATION = 9;
public const DEACTIVATING = 10;
public const ERROR_DEACTIVATING = 11;
public const COMPLETED = 12;
public static function is_valid_value( $state ) {
$valid_states = array(
self::NOT_STARTED,
self::STARTED,
self::ERROR_STARTED,
self::INSTALLING,
self::ERROR_INSTALLING,
self::ACTIVATING,
self::ERROR_ACTIVATING,
self::DB_MIGRATION,
self::ERROR_DB_MIGRATION,
self::DEACTIVATING,
self::ERROR_DEACTIVATING,
self::COMPLETED,
);
return in_array( $state, $valid_states, true );
}
}
@@ -0,0 +1,74 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Account_Settings_Controller' ) ) {
return;
}
class WC_REST_Connect_Account_Settings_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/account/settings';
/*
* @var WC_Connect_Payment_Methods_Store
*/
protected $payment_methods_store;
/**
* @var WC_Connect_Account_Settings
*/
protected $account_settings;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger, WC_Connect_Payment_Methods_Store $payment_methods_store ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->payment_methods_store = $payment_methods_store;
$this->account_settings = new WC_Connect_Account_Settings(
$settings_store,
$payment_methods_store
);
}
public function get() {
return new WP_REST_Response(
array_merge(
array( 'success' => true ),
$this->account_settings->get()
),
200
);
}
public function post( $request ) {
$settings = $request->get_json_params();
if ( ! $this->settings_store->can_user_manage_payment_methods() ) {
// Ignore the user-provided payment method ID if they don't have permission to change it
$old_settings = $this->settings_store->get_account_settings();
$settings['selected_payment_method_id'] = $old_settings['selected_payment_method_id'];
}
$result = $this->settings_store->update_account_settings( $settings );
if ( is_wp_error( $result ) ) {
$error = new WP_Error(
'save_failed',
sprintf(
__( 'Unable to update settings. %s', 'woocommerce-services' ),
$result->get_error_message()
),
array_merge(
array( 'status' => 400 ),
$result->get_error_data()
)
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
return new WP_REST_Response( array( 'success' => true ), 200 );
}
}
@@ -0,0 +1,70 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Address_Normalization_Controller' ) ) {
return;
}
class WC_REST_Connect_Address_Normalization_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/normalize-address';
public function post( $request ) {
$data = $request->get_json_params();
$address = $data['address'];
$phone = $address['phone'];
unset( $address['phone'] );
$body = array(
'destination' => $address,
);
$response = $this->api_client->send_address_normalization_request( $body );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
if ( isset( $response->field_errors ) ) {
$this->logger->log( 'Address validation errors: ' . implode( '; ', array_values( (array) $response->field_errors ) ), __CLASS__ );
return array(
'success' => true,
'field_errors' => $response->field_errors,
);
}
if ( ! isset( $response->normalized ) ) {
$response->normalized = new stdClass();
}
$response->normalized->phone = $phone;
$is_trivial_normalization = isset( $response->is_trivial_normalization ) ? $response->is_trivial_normalization : false;
return array(
'success' => true,
'normalized' => $response->normalized,
'is_trivial_normalization' => $is_trivial_normalization,
);
}
/**
* Validate the requester's permissions
*/
public function check_permission( $request ) {
$data = $request->get_json_params();
if ( 'origin' === $data['type'] ) {
return WC_Connect_Functions::user_can_manage_labels(); // Only an admin can normalize the origin address
}
return true; // non-authenticated service for the 'destination' address
}
}
@@ -0,0 +1,28 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Assets_Controller' ) ) {
return;
}
class WC_REST_Connect_Assets_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/assets';
public function get() {
return new WP_REST_Response(
array(
'success' => true,
'assets' => array(
'wc_connect_admin_script' => WC_Connect_Loader::get_wcs_admin_script_url(),
'wc_connect_admin_style' => WC_Connect_Loader::get_wcs_admin_style_url(),
),
),
200
);
}
}
@@ -0,0 +1,155 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Base_Controller' ) ) {
return;
}
abstract class WC_REST_Connect_Base_Controller extends WP_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* @var WC_Connect_API_Client
*/
protected $api_client;
/**
* @var WC_Connect_Service_Settings_Store
*/
protected $settings_store;
/**
* @var WC_Connect_Logger
*/
protected $logger;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger ) {
$this->api_client = $api_client;
$this->settings_store = $settings_store;
$this->logger = $logger;
}
public function register_routes() {
if ( method_exists( $this, 'get' ) ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'get_internal' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
if ( method_exists( $this, 'post' ) ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => 'POST',
'callback' => array( $this, 'post_internal' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
if ( method_exists( $this, 'put' ) ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => 'PUT',
'callback' => array( $this, 'put_internal' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
if ( method_exists( $this, 'delete' ) ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => 'DELETE',
'callback' => array( $this, 'delete_internal' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
}
/**
* Consolidate cache prevention mechanisms.
*/
public function prevent_route_caching() {
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', true ); // Play nice with WP-Super-Cache
}
// Prevent our REST API endpoint responses from being added to browser cache
add_filter( 'rest_post_dispatch', array( $this, 'send_nocache_header' ), PHP_INT_MAX, 2 );
}
/**
* Send a no-cache header for WCS REST API responses. Prompted by cache issues
* on the Pantheon hosting platform.
*
* See: https://pantheon.io/docs/cache-control/
*
* @param WP_REST_Response $response
* @param WP_REST_Server $server
*
* @return WP_REST_Response passthrough $response parameter
*/
public function send_nocache_header( $response, $server ) {
$server->send_header( 'Cache-Control', 'no-cache, must-revalidate, max-age=0' );
return $response;
}
public function get_internal( $request ) {
$this->prevent_route_caching();
return $this->get( $request );
}
public function post_internal( $request ) {
$this->prevent_route_caching();
return $this->post( $request );
}
public function put_internal( $request ) {
$this->prevent_route_caching();
return $this->put( $request );
}
public function delete_internal( $request ) {
$this->prevent_route_caching();
return $this->delete( $request );
}
/**
* Validate the requester's permissions
*/
public function check_permission( $request ) {
return WC_Connect_Functions::user_can_manage_labels();
}
}
@@ -0,0 +1,88 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Migration_Flag_Controller' ) ) {
return;
}
class WC_REST_Connect_Migration_Flag_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/migration-flag';
/**
* Tracks instance.
*
* @var WC_Connect_Tracks
*/
protected $tracks;
public function __construct( $api_client, $settings_store, $logger, $tracks ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->set_tracks( $tracks );
}
/**
* Set tracks instance.
*
* @param WC_Connect_Tracks $tracks Tracks instance.
*/
public function set_tracks( $tracks ) {
$this->tracks = $tracks;
}
public function post( $request ) {
$params = $request->get_json_params();
$migration_state = intval( $params['migration_state'] );
// If the migration state is greater than "COMPLETED", then we can assume that the next part of the migration
// state is being handled by WooCommerce Shipping.
// This shouldn't be necessary since our migration shouldn't be displayed to begin with, but we're adding
// support for it, just in case.
if ( $migration_state > WC_Connect_WCST_To_WCShipping_Migration_State_Enum::COMPLETED ) {
return new WP_REST_Response(
array(
'result' => __( 'WooCommerce Shipping has taken over the migration process.', 'woocommerce-services' ),
),
200
);
}
if ( ! WC_Connect_WCST_To_WCShipping_Migration_State_Enum::is_valid_value( $migration_state ) ) {
return new WP_Error(
'invalid_migration_state',
__( 'Invalid migration state. Can not update migration state.', 'woocommerce-services' ),
array( 'status' => 400 )
);
}
$existing_migration_state = get_option( 'wcshipping_migration_state' );
if ( $existing_migration_state && intval( $existing_migration_state ) === $migration_state ) {
return new WP_REST_Response( array( 'result' => __( 'Migration flag is the same, no changes needed.', 'woocommerce-services' ) ), 304 );
}
$result = update_option( 'wcshipping_migration_state', $migration_state );
if ( $result ) {
$this->tracks->record_user_event(
'migration_flag_state_update',
array(
'migration_state' => $migration_state,
'updated' => $result,
)
);
return new WP_REST_Response( array( 'result' => __( 'Migration flag updated successfully.', 'woocommerce-services' ) ), 200 );
}
$error = new WP_Error(
'wcst_to_wcshipping_migration_failed_to_update',
__( 'Unable to update migration flag. The flag could not be updated.', 'woocommerce-services' ),
array( 'status' => 500 )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
}
@@ -0,0 +1,151 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Packages_Controller' ) ) {
return;
}
class WC_REST_Connect_Packages_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/packages';
/**
* @var WC_Connect_Package_Settings
*/
protected $package_settings;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger, WC_Connect_Service_Schemas_Store $service_schemas_store ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->package_settings = new WC_Connect_Package_Settings(
$settings_store,
$service_schemas_store
);
}
public function get() {
return new WP_REST_Response(
array_merge(
array( 'success' => true ),
$this->package_settings->get()
),
200
);
}
/**
* Update the existing custom and predefined packages.
*
* @param WP_REST_Request $request The request body contains the custom/predefined packages to replace.
* @return WP_REST_Response
*/
public function put( $request ) {
$packages = $request->get_json_params();
$this->settings_store->update_packages( $packages['custom'] );
$this->settings_store->update_predefined_packages( $packages['predefined'] );
return new WP_REST_Response( array( 'success' => true ), 200 );
}
/**
* Create custom and/or predefined packages.
*
* @param WP_REST_Request $request The request body contains the custom/predefined packages to create.
* @return WP_Error|WP_REST_Response
*/
public function post( $request ) {
$packages = $request->get_json_params();
$custom_packages = isset( $packages['custom'] ) ? $packages['custom'] : array();
$predefined_packages = isset( $packages['predefined'] ) ? $packages['predefined'] : array();
// Handle new custom packages. The custom packages are structured as an array of packages as dictionaries.
if ( ! empty( $custom_packages ) ) {
// Validate that the new custom packages have unique names.
$map_package_name = function( $package ) {
return $package['name'];
};
$custom_package_names = array_map( $map_package_name, $custom_packages );
$unique_custom_package_names = array_unique( $custom_package_names );
if ( count( $unique_custom_package_names ) < count( $custom_package_names ) ) {
$duplicate_package_names = array_diff_assoc( $custom_package_names, $unique_custom_package_names );
$error = array(
'code' => 'duplicate_custom_package_names',
'message' => __( 'The new custom package names are not unique.' ),
'data' => array( 'package_names' => array_values( $duplicate_package_names ) ),
);
return new WP_REST_Response( $error, 400 );
}
// Validate that the new custom packages do not have the same names as existing custom packages.
$existing_custom_packages = $this->settings_store->get_packages();
$existing_custom_package_names = array_map( $map_package_name, $existing_custom_packages );
$duplicate_package_names = array_intersect( $existing_custom_package_names, $custom_package_names );
if ( ! empty( $duplicate_package_names ) ) {
$error = array(
'code' => 'duplicate_custom_package_names_of_existing_packages',
'message' => __( 'At least one of the new custom packages has the same name as existing packages.' ),
'data' => array( 'package_names' => array_values( $duplicate_package_names ) ),
);
return new WP_REST_Response( $error, 400 );
}
// If no duplicate custom packages, create the given packages.
$this->settings_store->create_packages( $custom_packages );
}
// Handle new predefined packages. The predefined packages are structured as a dictionary from carrier name to
// an array of package names.
if ( ! empty( $predefined_packages ) ) {
$duplicate_package_names_by_carrier = array();
// Validate that the new predefined packages have unique names for each carrier.
foreach ( $predefined_packages as $carrier => $package_names ) {
$unique_package_names = array_unique( $package_names );
if ( count( $unique_package_names ) < count( $package_names ) ) {
$duplicate_package_names = array_diff_assoc( $package_names, $unique_package_names );
$duplicate_package_names_by_carrier[ $carrier ] = array_values( $duplicate_package_names );
}
}
if ( ! empty( $duplicate_package_names_by_carrier ) ) {
$error = array(
'code' => 'duplicate_predefined_package_names',
'message' => __( 'The new predefined package names are not unique.' ),
'data' => array( 'package_names_by_carrier' => $duplicate_package_names_by_carrier ),
);
return new WP_REST_Response( $error, 400 );
}
// Validate that the new predefined packages for each carrier do not have the same names as existing predefined packages.
$existing_predefined_packages = $this->settings_store->get_predefined_packages();
if ( ! empty( $existing_predefined_packages ) ) {
foreach ( $existing_predefined_packages as $carrier => $existing_package_names ) {
$new_package_names = isset( $predefined_packages[ $carrier ] ) ? $predefined_packages[ $carrier ] : array();
$duplicate_package_names = array_intersect( $existing_package_names, $new_package_names );
if ( ! empty( $duplicate_package_names ) ) {
$duplicate_package_names_by_carrier[ $carrier ] = array_values( $duplicate_package_names );
}
}
}
if ( ! empty( $duplicate_package_names_by_carrier ) ) {
$error = array(
'code' => 'duplicate_predefined_package_names_of_existing_packages',
'message' => __( 'At least one of the new predefined packages has the same name as existing packages.' ),
'data' => array( 'package_names_by_carrier' => $duplicate_package_names_by_carrier ),
);
return new WP_REST_Response( $error, 400 );
}
// If no duplicate predefined packages, create the given packages.
$this->settings_store->create_predefined_packages( $predefined_packages );
}
return new WP_REST_Response( array( 'success' => true ), 200 );
}
}
@@ -0,0 +1,46 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Self_Help_Controller' ) ) {
return;
}
class WC_REST_Connect_Self_Help_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/self-help';
public function post( $request ) {
$settings = $request->get_json_params();
if (
empty( $settings )
|| ! array_key_exists( 'wcc_debug_on', $settings )
|| ! array_key_exists( 'wcc_logging_on', $settings )
) {
$error = new WP_Error(
'bad_form_data',
__( 'Unable to update settings. The form data could not be read.', 'woocommerce-services' ),
array( 'status' => 400 )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
if ( 1 == $settings['wcc_logging_on'] ) {
$this->logger->enable_logging();
} else {
$this->logger->disable_logging();
}
if ( 1 == $settings['wcc_debug_on'] ) {
$this->logger->enable_debug();
} else {
$this->logger->disable_debug();
}
return new WP_REST_Response( array( 'success' => true ), 200 );
}
}
@@ -0,0 +1,46 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Service_Data_Refresh_Controller' ) ) {
return;
}
class WC_REST_Connect_Service_Data_Refresh_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/service-data-refresh';
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $services_schemas_store;
public function set_service_schemas_store( $services_schemas_store ) {
$this->services_schemas_store = $services_schemas_store;
}
public function post() {
$result = $this->services_schemas_store->fetch_service_schemas_from_connect_server();
if ( $result === false ) {
return new WP_REST_Response(
[
'success' => false,
],
500
);
}
$schemas = $this->services_schemas_store->get_service_schemas();
return new WP_REST_Response(
[
'success' => true,
'timestamp' => $this->services_schemas_store->get_last_fetch_timestamp(),
'has_service_schemas' => ! is_null( $schemas ),
],
200
);
}
}
@@ -0,0 +1,104 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Services_Controller' ) ) {
return;
}
class WC_REST_Connect_Services_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/services/(?P<id>[a-z_]+)\/(?P<instance>[\d]+)';
/**
* @var WC_Connect_Service_Schemas_Store
*/
protected $service_schemas_store;
public function __construct(
WC_Connect_API_Client $api_client,
WC_Connect_Service_Settings_Store $settings_store,
WC_Connect_Logger $logger,
WC_Connect_Service_Schemas_Store $schemas_store
) {
parent::__construct( $api_client, $settings_store, $logger );
$this->service_schemas_store = $schemas_store;
}
public function get( $request ) {
$method_id = $request['id'];
$instance_id = isset( $request['instance'] ) ? $request['instance'] : false;
$service_schema = $this->service_schemas_store->get_service_schema_by_id_or_instance_id(
$instance_id
? $instance_id
: $method_id
);
if ( ! $service_schema ) {
return new WP_Error( 'schemas_not_found', __( 'Service schemas were not loaded', 'woocommerce-services' ), array( 'status' => 500 ) );
}
$payload = apply_filters(
'wc_connect_shipping_service_settings',
array(
'success' => true,
),
$method_id,
$instance_id
);
return new WP_REST_Response( $payload, 200 );
}
/**
* Attempts to update the settings on a particular service and instance
*/
public function post( $request ) {
$request_params = $request->get_params();
$id = array_key_exists( 'id', $request_params ) ? $request_params['id'] : '';
$instance = array_key_exists( 'instance', $request_params ) ? absint( $request_params['instance'] ) : false;
if ( empty( $id ) ) {
$error = new WP_Error(
'service_id_missing',
__( 'Unable to update service settings. Form data is missing service ID.', 'woocommerce-services' ),
array( 'status' => 400 )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$settings = (object) $request->get_json_params();
if ( empty( $settings ) ) {
$error = new WP_Error(
'bad_form_data',
__( 'Unable to update service settings. The form data could not be read.', 'woocommerce-services' ),
array( 'status' => 400 )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$validation_result = $this->settings_store->validate_and_possibly_update_settings( $settings, $id, $instance );
if ( is_wp_error( $validation_result ) ) {
$error = new WP_Error(
'validation_failed',
sprintf(
__( 'Unable to update service settings. Validation failed. %s', 'woocommerce-services' ),
$validation_result->get_error_message()
),
array( 'status' => 400 )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
return new WP_REST_Response( array( 'success' => true ), 200 );
}
}
@@ -0,0 +1,33 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Shipping_Carrier_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Carrier_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/shipping/carrier';
public function post( $request ) {
$settings = $request->get_json_params();
$response = $this->api_client->create_shipping_carrier_account( $settings );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
do_action( 'wc_connect_fetch_service_schemas' );
return $response;
}
}
@@ -0,0 +1,31 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Shipping_Carrier_Delete_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Carrier_Delete_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/shipping/carrier/(?P<carrier_id>.+)';
public function delete( $request ) {
$carrier_id = $request['carrier_id'];
$response = $this->api_client->disconnect_carrier_account( $carrier_id );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
do_action( 'wc_connect_fetch_service_schemas' );
return array( 'success' => true );
}
}
@@ -0,0 +1,42 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Shipping_Carrier_Types_Controller' ) ) {
return;
}
/**
* Retrieve a list of carrier WooCommerce Shipping & Tax supports, along with the
* fields needed for each carrier in order to do carrier account registration.
*/
class WC_REST_Connect_Shipping_Carrier_Types_Controller extends WC_REST_Connect_Base_Controller {
/**
* Carrier-types end point
*
* @var string
*/
protected $rest_base = 'connect/shipping/carrier-types';
/**
* GET request
*
* @return WP_REST_Response|WP_Error
*/
public function get() {
$response = $this->api_client->get_carrier_types();
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return $response;
}
return new WP_REST_Response(
[
'success' => true,
'carriers' => $response->carriers,
]
);
}
}
@@ -0,0 +1,28 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Shipping_Carriers_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Carriers_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/shipping/carriers';
public function get() {
$response = $this->api_client->get_all_shipping_carriers();
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
return $response;
}
}
@@ -0,0 +1,315 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Label_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Label_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/(?P<order_id>\d+)';
/*
* @var WC_Connect_Shipping_Label
*/
protected $shipping_label;
/*
* @var WC_Connect_Payment_Methods_Store
*/
protected $payment_methods_store;
public function __construct( WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger, WC_Connect_Shipping_Label $shipping_label, WC_Connect_Payment_Methods_Store $payment_methods_store ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->shipping_label = $shipping_label;
$this->payment_methods_store = $payment_methods_store;
}
public function register_routes() {
parent::register_routes();
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/creation_eligibility',
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'get_creation_eligibility' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
public function get( $request ) {
$order_id = $request['order_id'];
$payload = $this->shipping_label->get_label_payload( $order_id );
if ( ! $payload ) {
return new WP_Error( 'not_found', __( 'Order not found', 'woocommerce-services' ), array( 'status' => 404 ) );
}
$payload['success'] = true;
return new WP_REST_Response( $payload, 200 );
}
public function post( $request ) {
$settings = $request->get_json_params();
$order_id = $request['order_id'];
$settings['order_id'] = $order_id;
if ( empty( $settings['payment_method_id'] ) || ! $this->settings_store->can_user_manage_payment_methods() ) {
$settings['payment_method_id'] = $this->settings_store->get_selected_payment_method_id();
} else {
$this->settings_store->set_selected_payment_method_id( $settings['payment_method_id'] );
}
$last_box_id = '';
$last_service_id = '';
$last_carrier_id = '';
$service_names = array();
foreach ( $settings['packages'] as $index => $package ) {
$service_names[] = $package['service_name'];
unset( $package['service_name'] );
$settings['packages'][ $index ] = $package;
if ( empty( $last_box_id ) && ! empty( $package['box_id'] ) ) {
$last_box_id = $package['box_id'];
}
if ( empty( $last_service_id ) && ! empty( $package['service_id'] ) ) {
$last_service_id = $package['service_id'];
}
if ( empty( $last_carrier_id ) && ! empty( $package['carrier_id'] ) ) {
$last_carrier_id = $package['carrier_id'];
}
}
if ( ! empty( $last_box_id ) && 'individual' !== $last_box_id ) {
update_user_meta( get_current_user_id(), 'wc_connect_last_box_id', $last_box_id );
}
if ( ! empty( $last_service_id ) && '' !== $last_service_id ) {
update_user_meta( get_current_user_id(), 'wc_connect_last_service_id', $last_service_id );
}
if ( ! empty( $last_carrier_id ) && '' !== $last_carrier_id ) {
update_user_meta( get_current_user_id(), 'wc_connect_last_carrier_id', $last_carrier_id );
}
$response = $this->api_client->send_shipping_label_request( $settings );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$label_ids = array();
$purchased_labels_meta = array();
$package_lookup = $this->settings_store->get_package_lookup();
foreach ( $response->labels as $index => $label_data ) {
if ( isset( $label_data->error ) ) {
$error = new WP_Error(
$label_data->error->code,
$label_data->error->message,
array( 'message' => $label_data->error->message )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$label_ids[] = $label_data->label->label_id;
$label_meta = array(
'label_id' => $label_data->label->label_id,
'tracking' => $label_data->label->tracking_id,
'refundable_amount' => $label_data->label->refundable_amount,
'created' => $label_data->label->created,
'carrier_id' => $label_data->label->carrier_id,
'service_name' => $service_names[ $index ],
'status' => $label_data->label->status,
'commercial_invoice_url' => $label_data->label->commercial_invoice_url ?? '',
'is_commercial_invoice_submitted_electronically' => $label_data->label->is_commercial_invoice_submitted_electronically ?? '',
);
$package = $settings['packages'][ $index ];
$box_id = $package['box_id'];
if ( 'individual' === $box_id ) {
$label_meta['package_name'] = __( 'Individual packaging', 'woocommerce-services' );
} elseif ( isset( $package_lookup[ $box_id ] ) ) {
$label_meta['package_name'] = $package_lookup[ $box_id ]['name'];
} else {
$label_meta['package_name'] = __( 'Unknown package', 'woocommerce-services' );
}
$label_meta['is_letter'] = isset( $package['is_letter'] ) ? $package['is_letter'] : false;
$product_names = array();
$product_ids = array();
foreach ( $package['products'] as $product_id ) {
$product = wc_get_product( $product_id );
$product_ids[] = $product_id;
if ( $product ) {
$product_names[] = $product->get_title();
} else {
$order = wc_get_order( $order_id );
$product_names[] = WC_Connect_Utils::get_product_name_from_order( $product_id, $order );
}
}
$label_meta['product_names'] = $product_names;
$label_meta['product_ids'] = $product_ids;
array_unshift( $purchased_labels_meta, $label_meta );
}
$this->settings_store->add_labels_to_order( $order_id, $purchased_labels_meta );
return array(
'labels' => $purchased_labels_meta,
'success' => true,
);
}
/**
* Available params for $request:
* - `can_create_payment_method: Boolean`: optional with default value `true`. If `false`, at least one existing payment method is
* required for label creation.
* - `can_create_package: Boolean`: optional with default value `true`. If `false`, at least one pre-existing
* package (custom or predefined) is required for label creation.
* - `can_create_customs_form: Boolean`: optional with default value `true`. If `false`, the order is eligible for
* label creation if a customs form is not required for the origin and destination address in the US.
*
* @param WP_REST_Request $request API request with optional parameters as above.
* @return WP_REST_Response
*/
public function get_creation_eligibility( $request ) {
$order_id = $request['order_id'];
$order = wc_get_order( $order_id );
if ( ! $order ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'order_not_found',
),
200
);
}
// Shipping labels should be enabled in account settings.
if ( true !== $this->settings_store->get_account_settings()['enabled'] ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'account_settings_disabled',
),
200
);
}
// Check if the store is eligible for shipping label creation.
if ( ! $this->shipping_label->is_store_eligible_for_shipping_label_creation() ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'store_not_eligible',
),
200
);
}
// If the client cannot create a customs form:
// - The store address has to be in the US.
// - The origin and destination addresses have to be in the US.
$client_can_create_customs_form = isset( $request['can_create_customs_form'] ) ? filter_var( $request['can_create_customs_form'], FILTER_VALIDATE_BOOLEAN ) : true;
$store_country = wc_get_base_location()['country'];
if ( ! $client_can_create_customs_form ) {
// The store address has to be in the US.
if ( 'US' !== $store_country ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'store_country_not_supported_when_customs_form_is_not_supported_by_client',
),
200
);
}
// The origin and destination addresses have to be in the US.
$origin_address = $this->settings_store->get_origin_address();
$destination_address = $order->get_address( 'shipping' );
if ( 'US' !== $origin_address['country'] || 'US' !== $destination_address['country'] ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'origin_or_destination_country_not_supported_when_customs_form_is_not_supported_by_client',
),
200
);
}
}
// If the client cannot create a package (`can_create_package` param is set to `false`), a pre-existing package
// is required.
$client_can_create_package = isset( $request['can_create_package'] ) ? filter_var( $request['can_create_package'], FILTER_VALIDATE_BOOLEAN ) : true;
if ( ! $client_can_create_package ) {
if ( empty( $this->settings_store->get_packages() ) && empty( $this->settings_store->get_predefined_packages() ) ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'no_packages_when_client_cannot_create_package',
),
200
);
}
}
// There is at least one non-refunded and shippable product.
if ( ! $this->shipping_label->is_order_eligible_for_shipping_label_creation( $order ) ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'order_not_eligible',
),
200
);
}
// If the client cannot create a payment method (`can_create_payment_method` param is set to `false`), an existing payment method is required.
$client_can_create_payment_method = isset( $request['can_create_payment_method'] ) ? filter_var( $request['can_create_payment_method'], FILTER_VALIDATE_BOOLEAN ) : true;
if ( ! $client_can_create_payment_method && empty( $this->payment_methods_store->get_payment_methods() ) ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'no_payment_methods_and_client_cannot_create_one',
),
200
);
}
// There is a pre-selected payment method or the user can manage payment methods.
if ( ! ( $this->settings_store->get_selected_payment_method_id() || $this->settings_store->can_user_manage_payment_methods() ) ) {
return new WP_REST_Response(
array(
'is_eligible' => false,
'reason' => 'no_selected_payment_method_and_user_cannot_manage_payment_methods',
),
200
);
}
return new WP_REST_Response(
array(
'is_eligible' => true,
),
200
);
}
}
@@ -0,0 +1,40 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Label_Preview_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Label_Preview_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/preview';
public function get( $request ) {
$raw_params = $request->get_params();
$params = array();
$params['paper_size'] = $raw_params['paper_size'];
$this->settings_store->set_preferred_paper_size( $params['paper_size'] );
$params['carrier'] = 'usps';
$params['labels'] = array();
$captions = empty( $raw_params['caption_csv'] ) ? array() : explode( ',', $raw_params['caption_csv'] );
foreach ( $captions as $caption ) {
$params['labels'][] = array( 'caption' => urldecode( $caption ) );
}
$raw_response = $this->api_client->get_labels_preview_pdf( $params );
if ( is_wp_error( $raw_response ) ) {
$this->logger->log( $raw_response, __CLASS__ );
return $raw_response;
}
header( 'content-type: ' . $raw_response['headers']['content-type'] );
echo $raw_response['body']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
die();
}
}
@@ -0,0 +1,69 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Label_Print_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Label_Print_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/print';
public function get( $request ) {
$raw_params = $request->get_params();
$params = array();
$params['paper_size'] = $raw_params['paper_size'];
$this->settings_store->set_preferred_paper_size( $params['paper_size'] );
$label_ids = ! empty( $raw_params['label_id_csv'] ) ? explode( ',', $raw_params['label_id_csv'] ) : array();
$n_label_ids = count( $label_ids );
$captions = ! empty( $raw_params['caption_csv'] ) ? explode( ',', $raw_params['caption_csv'] ) : array();
$n_captions = count( $captions );
// Either there are the same number of captions as labels, or no captions at all
if ( ! $n_label_ids || ( $n_captions && $n_captions !== $n_label_ids ) ) {
$message = __( 'Invalid PDF request.', 'woocommerce-services' );
$error = new WP_Error(
'invalid_pdf_request',
$message,
array(
'message' => $message,
'status' => 400,
)
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$params['labels'] = array();
for ( $i = 0; $i < $n_label_ids; $i++ ) {
$params['labels'][ $i ] = array();
$params['labels'][ $i ]['label_id'] = (int) $label_ids[ $i ];
if ( $n_captions ) {
$params['labels'][ $i ]['caption'] = urldecode( $captions[ $i ] );
}
}
$raw_response = $this->api_client->get_labels_print_pdf( $params );
if ( is_wp_error( $raw_response ) ) {
$this->logger->log( $raw_response, __CLASS__ );
return $raw_response;
}
if ( isset( $raw_params['json'] ) && $raw_params['json'] ) {
return array(
'mimeType' => $raw_response['headers']['content-type'],
'b64Content' => base64_encode( $raw_response['body'] ),
'success' => true,
);
} else {
header( 'content-type: ' . $raw_response['headers']['content-type'] );
echo $raw_response['body']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
die();
}
}
}
@@ -0,0 +1,48 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Label_Refund_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Label_Refund_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/(?P<order_id>\d+)/(?P<label_id>\d+)/refund';
public function post( $request ) {
$response = $this->api_client->send_shipping_label_refund_request( $request['label_id'] );
if ( isset( $response->error ) ) {
$response = new WP_Error(
property_exists( $response->error, 'code' ) ? $response->error->code : 'refund_error',
property_exists( $response->error, 'message' ) ? $response->error->message : ''
);
}
if ( is_wp_error( $response ) ) {
$response->add_data(
array(
'message' => $response->get_error_message(),
),
$response->get_error_code()
);
$this->logger->log( $response, __CLASS__ );
return $response;
}
$label_refund = (object) array(
'label_id' => (int) $response->label->id,
'refund' => $response->refund,
);
$this->settings_store->update_label_order_meta_data( $request['order_id'], $label_refund );
return array(
'success' => true,
'refund' => $response->refund,
);
}
}
@@ -0,0 +1,38 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Label_Status_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Label_Status_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/(?P<order_id>\d+)/(?P<label_ids>(\d+)(,\d+)*)';
public function get( $request ) {
$label_ids = explode( ',', $request['label_ids'] );
$labels = array();
foreach ( $label_ids as $label_id ) {
$response = $this->api_client->get_label_status( $label_id );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
$label = $this->settings_store->update_label_order_meta_data( $request['order_id'], $response->label );
$labels[] = $label;
}
return array(
'success' => true,
'labels' => $labels,
);
}
}
@@ -0,0 +1,172 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Shipping_Rates_Controller' ) ) {
return;
}
class WC_REST_Connect_Shipping_Rates_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/label/(?P<order_id>\d+)/rates';
/**
* Prefix to add in package name for making requests with multiple rates.
*/
public $SPECIAL_RATE_PREFIX = '_wcs_rate_type_';
/**
* Array of extra options to collect rates for.
*/
protected $extra_rates = array(
'signature_required' => array(
'signature' => 'yes',
),
'adult_signature_required' => array(
'signature' => 'adult',
),
);
private function has_customs_data( $package ) {
return isset( $package['contents_type'] );
}
/**
*
* @param WP_REST_Request $request - See WC_Connect_API_Client::get_label_rates()
* @return array|WP_Error
*/
public function post( $request ) {
$payload = $request->get_json_params();
$payload['payment_method_id'] = $this->settings_store->get_selected_payment_method_id();
$order_id = $request['order_id'];
// This is the earliest point in the printing label flow where we are sure that
// the merchant wants to ship from this exact address (normalized or otherwise)
$this->settings_store->update_origin_address( $payload['origin'] );
$this->settings_store->update_destination_address( $order_id, $payload['destination'] );
// Update the customs information on all this order's products
$updated_product_ids = array();
foreach ( $payload['packages'] as $package_id => $package ) {
if ( ! $this->has_customs_data( $package ) ) {
break;
}
foreach ( $package['items'] as $index => $item ) {
if ( ! isset( $updated_product_ids[ $item['product_id'] ] ) ) {
$updated_product_ids[ $item['product_id'] ] = true;
update_post_meta(
$item['product_id'],
'wc_connect_customs_info',
array(
'description' => $item['description'],
'hs_tariff_number' => $item['hs_tariff_number'],
'origin_country' => $item['origin_country'],
)
);
}
}
}
$response = $this->get_all_rates( $payload );
if ( is_wp_error( $response ) ) {
return $response;
}
return array(
'success' => true,
'rates' => $response,
);
}
/**
* Get standard rates along with rates for special options
* that are defined in $this->extra_rates
*
* @param stdClass $payload Request payload.
* @return WPError|stdClass
*/
public function get_all_rates( $payload ) {
$signature_packages = [];
$original_package_names = [];
// Add extra package requests with special options set.
foreach ( $this->extra_rates as $rate_name => $rate_option ) {
foreach ( $rate_option as $option_name => $option_value ) {
foreach ( $payload['packages'] as $package_id => $package ) {
$original_package_names[] = $package['id'];
$new_package = $package;
$new_package[ $option_name ] = $option_value;
$new_package['id'] .= $this->SPECIAL_RATE_PREFIX . $rate_name;
$signature_packages[] = $new_package;
}
}
}
$payload['packages'] = array_merge( $payload['packages'], $signature_packages );
$response = $this->request_rates( $payload );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( property_exists( $response, 'rates' ) ) {
return $this->merge_all_rates( $response->rates, $original_package_names );
}
return new stdClass();
}
/**
* Merge default rates together with extra rates.
*
* get_all_rates requests extra rate options as separate
* packages. This function groups these separate packages
* under the original the package name for easier parsing
* on the frontend.
*
* @param stdClass $rates Rate response for server.
* @param array $original_package_names Package names.
*
* @return array Rates
*/
public function merge_all_rates( $rates, $original_package_names ) {
$parsed_rates = [];
foreach ( $original_package_names as $name ) {
// Add a 'default' entry for the rate with no special options.
$parsed_rates[ $name ] = array(
'default' => $rates->{ $name },
);
// Get package for each extra rate to group them under the original package name.
foreach ( $this->extra_rates as $extra_rate_name => $option ) {
$extra_rate_package_name = $name . $this->SPECIAL_RATE_PREFIX . $extra_rate_name;
if ( isset( $rates->{ $extra_rate_package_name } ) ) {
$parsed_rates[ $name ][ $extra_rate_name ] = $rates->{ $extra_rate_package_name };
}
}
}
return $parsed_rates;
}
/**
* Make rate request.
*
* @param stdClass $payload Request payload.
* @return WPError|stdClass
*/
public function request_rates( $payload ) {
$response = $this->api_client->get_label_rates( $payload );
if ( is_wp_error( $response ) ) {
$error = new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array( 'message' => $response->get_error_message() )
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
return $response;
}
}
@@ -0,0 +1,38 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Subscription_Activate_Controller' ) ) {
return;
}
class WC_REST_Connect_Subscription_Activate_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/subscription/(?P<subscription_key>.+)/activate';
public function post( $request ) {
$subscription_key = $request['subscription_key'];
$response = $this->api_client->activate_subscription( $subscription_key );
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return $response;
}
$activated = wp_remote_retrieve_response_code( $activation_response ) === 200;
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) ) {
return new WP_Error(
'already_active',
__( 'The subscription is already active.', 'woocommerce-services' )
);
}
return new WP_REST_Response(
array(
'success' => true,
)
);
}
}
@@ -0,0 +1,28 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit();
}
if ( class_exists( 'WC_REST_Connect_Subscriptions_Controller' ) ) {
return;
}
class WC_REST_Connect_Subscriptions_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/subscriptions';
public function post() {
$response = $this->api_client->get_wccom_subscriptions();
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return $response;
}
return new WP_REST_Response(
array(
'success' => true,
'subscriptions' => $response->subscriptions,
)
);
}
}
@@ -0,0 +1,51 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Tos_Controller' ) ) {
return;
}
class WC_REST_Connect_Tos_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/tos';
public function get() {
return new WP_REST_Response(
array(
'success' => true,
'accepted' => WC_Connect_Options::get_option( 'tos_accepted' ),
),
200
);
}
public function post( $request ) {
$settings = $request->get_json_params();
if ( ! $settings || ! isset( $settings['accepted'] ) || ! $settings['accepted'] ) {
return new WP_Error( 'bad_request', __( 'Bad request', 'woocommerce-services' ), array( 'status' => 400 ) );
}
WC_Connect_Options::update_option( 'tos_accepted', true );
return new WP_REST_Response(
array(
'success' => true,
'accepted' => WC_Connect_Options::get_option( 'tos_accepted' ),
),
200
);
}
/**
* Validate the requester's permissions
*/
public function check_permission( $request ) {
return current_user_can( 'manage_woocommerce' ) &&
current_user_can( 'install_plugins' ) &&
current_user_can( 'activate_plugins' );
}
}
@@ -0,0 +1,19 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_WCShipping_Compatibility_Packages_Controller' ) ) {
return;
}
require_once __DIR__ . '/class-wc-rest-connect-packages-controller.php';
require_once __DIR__ . '/class-wc-rest-connect-wcshipping-compatibility-packages-controller.php';
/**
* REST controller using WCS&T's settings store instead of WCShipping's.
*/
class WC_REST_Connect_WCShipping_Compatibility_Packages_Controller extends WC_REST_Connect_Packages_Controller {
protected $rest_base = 'connect/wcservices/packages';
}
@@ -0,0 +1,290 @@
<?php
/**
* REST API Data controller.
*
* Handles requests to the /data/continents endpoint.
*
* Directly copied from the wc-api-dev plugin. Delete this when the "v3" REST API is included in all the WC versions we support.
*
* @author Automattic
* @category API
* @package WooCommerce/API
* @since 3.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Data controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Controller
*/
class WC_REST_Dev_Data_Continents_Controller extends WC_REST_Dev_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* @var WC_Connect_Continents
*/
protected $continents;
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data/continents';
public function __construct() {
$this->continents = new WC_Connect_Continents();
}
/**
* Register routes.
*
* @since 3.1.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<location>[\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'continent' => array(
'description' => __( '2 character continent code.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Return the list of states for all continents.
*
* @since 3.1.0
* @param WP_REST_Request $request
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$continents = WC()->countries->get_continents();
$data = array();
foreach ( array_keys( $continents ) as $continent_code ) {
$continent = $this->continents->get_continent( $continent_code, $request );
$response = $this->prepare_item_for_response( $continent, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
return rest_ensure_response( $data );
}
/**
* Return the list of locations for a given continent.
*
* @since 3.1.0
* @param WP_REST_Request $request
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$data = $this->continents->get_continent( strtoupper( $request['location'] ), $request );
if ( empty( $data ) ) {
return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $data, $request );
}
/**
* Prepare the data object for response.
*
* @since 3.1.0
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter the location list returned from the API.
*
* Allows modification of the loction data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original list of continent(s), countries, and states.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given continent.
*/
protected function prepare_links( $item ) {
$continent_code = strtolower( $item['code'] );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the location schema, conforming to JSON Schema.
*
* @since 3.1.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_continents',
'type' => 'object',
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( '2 character continent code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of continent.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'countries' => array(
'type' => 'array',
'description' => __( 'List of countries on this continent.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'currency_code' => array(
'type' => 'string',
'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'currency_pos' => array(
'type' => 'string',
'description' => __( 'Currency symbol position for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'decimal_sep' => array(
'type' => 'string',
'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'dimension_unit' => array(
'type' => 'string',
'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'num_decimals' => array(
'type' => 'integer',
'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'states' => array(
'type' => 'array',
'description' => __( 'List of states in this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'State code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of state.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
'thousand_sep' => array(
'type' => 'string',
'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'weight_unit' => array(
'type' => 'string',
'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
@@ -0,0 +1,192 @@
<?php
/**
* REST API Data controller.
*
* Handles requests to the /data endpoint.
*
* Directly copied from the wc-api-dev plugin. Delete this when the "v3" REST API is included in all the WC versions we support.
*
* @author Automattic
* @category API
* @package WooCommerce/API
* @since 3.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Data controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Controller
*/
class WC_REST_Dev_Data_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data';
/**
* Register routes.
*
* @since 3.1.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to read site data.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to read site settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Return the list of data resources.
*
* @since 3.1.0
* @param WP_REST_Request $request
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$data = array();
$resources = array(
array(
'slug' => 'continents',
'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ),
),
array(
'slug' => 'countries',
'description' => __( 'List of supported states in a given country.', 'woocommerce' ),
),
array(
'slug' => 'currencies',
'description' => __( 'List of supported currencies.', 'woocommerce' ),
),
);
foreach ( $resources as $resource ) {
$item = $this->prepare_item_for_response( (object) $resource, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Prepare a data resource object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $resource, $request ) {
$data = array(
'slug' => $resource->slug,
'description' => $resource->description,
);
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, 'view' );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $resource ) );
return $response;
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given country.
*/
protected function prepare_links( $item ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the data index schema, conforming to JSON Schema.
*
* @since 3.1.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_index',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Data resource ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Data resource description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}