Added login request

This commit is contained in:
Nedim Uka
2018-06-20 18:03:43 +02:00
parent 4e52521fae
commit 593b445a21
4716 changed files with 1218265 additions and 57 deletions

View File

@@ -0,0 +1,600 @@
<?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' ) ) {
class WC_Connect_API_Client {
/**
* @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 Services Server
*
* @return array|WP_Error
*/
public function get_service_schemas() {
$response_body = $this->request( 'POST', '/services' );
if ( is_wp_error( $response_body ) ) {
return $response_body;
}
$result = $this->validator->validate_service_schemas( $response_body );
if ( is_wp_error( $result ) ) {
return $result;
}
return $response_body;
}
/**
* Validates the settings for a given service with the WooCommerce Services 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 underscores or letters
if ( 1 === preg_match( '/[^a-z_]/i', $service_slug ) ) {
return new WP_Error( 'invalid_service_slug', 'Invalid WooCommerce Services service slug provided' );
}
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.", $product->get_id() ),
array( 'product_id' => $product->get_id() )
);
}
$weight = $product->get_weight();
$height = 0;
$length = 0;
$width = 0;
if ( $product->has_dimensions() ) {
$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 Services 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.' );
}
// 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 );
}
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 Services server for an array of payment methods
*
* @return mixed|WP_Error
*/
public function get_payment_methods() {
return $this->request( 'POST', '/payment/methods' );
}
/**
* Gets shipping rates (for labels) from the WooCommerce Services 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' );
}
/**
* Tests the connection to the WooCommerce Services Server
*
* @return true|WP_Error
*/
public function auth_test() {
return $this->request( 'GET', '/connection/test' );
}
/**
* Create a deferred Stripe Standard Account
* @param $email string The user's email address
* @param $country string The user's country
* @return object|WP_Error
*/
public function create_stripe_account( $email, $country ) {
$request = array(
'email' => $email,
'country' => $country,
);
return $this->request( 'POST', '/stripe/account', $request );
}
public function get_stripe_account_details() {
return $this->request( 'GET', '/stripe/account' );
}
public function get_stripe_oauth_init( $return_url ) {
$request = array(
'returnUrl' => $return_url,
);
return $this->request( 'POST', '/stripe/oauth-init', $request );
}
public function get_stripe_oauth_keys( $code ) {
$request = array(
'code' => $code,
);
return $this->request( 'POST', '/stripe/oauth-keys', $request );
}
public function deauthorize_stripe_account() {
return $this->request( 'POST', '/stripe/account/deauthorize' );
}
/**
* Sends a request to the WooCommerce Services Server
*
* @param $method
* @param $path
* @param $body
* @return mixed|WP_Error
*/
protected function request( $method, $path, $body = array() ) {
// TODO - incorporate caching for repeated identical requests
if ( ! class_exists( 'Jetpack_Data' ) ) {
return new WP_Error( 'jetpack_data_class_not_found', 'Unable to send request to WooCommerce Services server. Jetpack_Data was not found.' );
}
if ( ! method_exists( 'Jetpack_Data', 'get_access_token' ) ) {
return new WP_Error( 'jetpack_data_get_access_token_not_found', 'Unable to send request to WooCommerce Services server. Jetpack_Data does not implement get_access_token.' );
}
if ( ! is_array( $body ) ) {
return new WP_Error(
'request_body_should_be_array',
'Unable to send request to WooCommerce Services server. Body must be an array.'
);
}
$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 Services server.'
);
}
}
$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 Services server returned HTTP code: %d', $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 Services server returned ( %d ) and an empty response body.',
$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' ) ? $response_body->data : '';
return new WP_Error(
'wcc_server_error_response',
sprintf(
'Error: The WooCommerce Services server returned: %s %s ( %d )',
$error,
$message,
$response_code
),
$data
);
}
return $response_body;
}
/**
* 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, '/' );
$args['headers']['Authorization'] = $this->authorization_header();
$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' => JETPACK__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(),
) );
return $body;
}
/**
* Generates headers for our request to the WooCommerce Services 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.v1';
$headers[ 'Authorization' ] = $authorization;
return $headers;
}
protected function authorization_header() {
$token = Jetpack_Data::get_access_token( 0 );
$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 Services server. Jetpack Token is missing' );
}
if ( false === strpos( $token->secret, '.' ) ) {
return new WP_Error( 'invalid_token', 'Unable to send request to WooCommerce Services server. Jetpack Token is malformed.' );
}
list( $token_key, $token_secret ) = explode( '.', $token->secret );
$token_key = sprintf( '%s:%d:%d', $token_key, 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;
}
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 Services server. The timestamp generated for the signature is too old.' );
}
$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 )
) );
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* A class for working around the quirks and different versions of WordPress/WooCommerce
* This is for versions 2.6 and lower
*/
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Compatibility_WC26' ) ) {
class WC_Connect_Compatibility_WC26 extends WC_Connect_Compatibility {
/**
* Get the ID for a given Order.
*
* @param WC_Order $order
*
* @return int
*/
public function get_order_id( WC_Order $order ) {
return $order->id;
}
/**
* Get admin url for a given order
*
* @param WC_Order $order
*
* @return string
*/
public function get_edit_order_url( WC_Order $order ) {
return get_admin_url( null, 'post.php?post=' . $this->get_order_id( $order ) . '&action=edit' );
}
/**
* Get the payment method for a given Order.
*
* @param WC_Order $order
*
* @return string
*/
public function get_payment_method( WC_Order $order ) {
return $order->payment_method;
}
/**
* Retrieve the corresponding Product for the given Order Item.
*
* @param WC_Order $order
* @param WC_Order_Item|WC_Order_Item_Product|array $item
*
* @return WC_Product
*/
public function get_item_product( WC_Order $order, $item ) {
return $order->get_product_from_item( $item );
}
/**
* Get formatted list of Product Variations, if applicable.
*
* @param WC_Product_Variation $product
* @param bool $flat
*
* @return string
*/
public function get_formatted_variation( WC_Product_Variation $product, $flat = false ) {
return $product->get_formatted_variation_attributes( $flat );
}
/**
* Get the most specific ID for a given Product.
*
* Note: Returns the Variation ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
public function get_product_id( WC_Product $product ) {
return ( $product->is_type( 'variation' ) ) ? $product->variation_id : $product->get_id();
}
/**
* Get the top-level ID for a given Product.
*
* Note: Returns the Parent ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
public function get_parent_product_id( WC_Product $product ) {
return ( $product->is_type( 'variation' ) ) ? $product->parent->get_id() : $product->get_id();
}
/**
* 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
* @return string The product (or variation) name, ready to print
*/
public function get_product_name_from_order( $product_id, $order ) {
foreach ( $order->get_items() as $line_item ) {
if ( (int) $line_item[ 'product_id' ] === $product_id || (int) $line_item[ 'variation_id' ] === $product_id ) {
/* translators: %1$d: Product ID, %2$s: Product Name */
return sprintf( __( '#%1$d - %2$s', 'woocommerce-services' ), $product_id, $line_item[ 'name' ] );
}
}
/* translators: %d: Deleted Product ID */
return sprintf( __( '#%d - [Deleted product]', 'woocommerce-services' ), $product_id );
}
/**
* For a given product, return it's name. In supported versions, variable
* products will include their attributes.
*
* @param WC_Product $product Product (variable, simple, etc)
* @return string The product (or variation) name, ready to print
*/
public function get_product_name( WC_Product $product ) {
return $product->get_title();
}
}
}

View File

@@ -0,0 +1,145 @@
<?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' ) ) {
class WC_Connect_Compatibility_WC30 extends WC_Connect_Compatibility {
/**
* Get the ID for a given Order.
*
* @param WC_Order $order
*
* @return int
*/
public function get_order_id( WC_Order $order ) {
return $order->get_id();
}
/**
* Get admin url for a given order
*
* @param WC_Order $order
*
* @return string
*/
public function get_edit_order_url( WC_Order $order ) {
return $order->get_edit_order_url();
}
/**
* Get the payment method for a given Order.
*
* @param WC_Order $order
*
* @return string
*/
public function get_payment_method( WC_Order $order ) {
return $order->get_payment_method();
}
/**
* Retrieve the corresponding Product for the given Order Item.
*
* @param WC_Order $order
* @param WC_Order_Item|WC_Order_Item_Product|array $item
*
* @return WC_Product
*/
public function get_item_product( WC_Order $order, $item ) {
if ( is_array( $item ) ) {
return wc_get_product( $item[ 'product_id' ] );
}
return $item->get_product();
}
/**
* Get formatted list of Product Variations, if applicable.
*
* @param WC_Product_Variation $product
* @param bool $flat
*
* @return string
*/
public function get_formatted_variation( WC_Product_Variation $product, $flat = false ) {
return wc_get_formatted_variation( $product, $flat );
}
/**
* Get the most specific ID for a given Product.
*
* Note: Returns the Variation ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
public function get_product_id( WC_Product $product ) {
return $product->get_id();
}
/**
* Get the top-level ID for a given Product.
*
* Note: Returns the Parent ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
public function get_parent_product_id( WC_Product $product ) {
return ( $product->is_type( 'variation' ) ) ? $product->get_parent_id() : $product->get_id();
}
/**
* 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
* @return string The product (or variation) name, ready to print
*/
public function get_product_name_from_order( $product_id, $order ) {
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 ) {
$line_product_id = (int) get_metadata( 'order_item', $line_item->get_id(), '_product_id', true );
}
if ( ! $line_variation_id ) {
$line_variation_id = (int) get_metadata( 'order_item', $line_item->get_id(), '_variation_id', true );
}
if ( $line_product_id === $product_id || $line_variation_id === $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() );
}
}
/* translators: %d: Deleted Product ID */
return sprintf( __( '#%d - [Deleted product]', 'woocommerce-services' ), $product_id );
}
/**
* For a given product, return it's name. In supported versions, variable
* products will include their attributes.
*
* @param WC_Product $product Product (variable, simple, etc)
* @return string The product (or variation) name, ready to print
*/
public function get_product_name( WC_Product $product ) {
return $product->get_name();
}
}
}

View File

@@ -0,0 +1,142 @@
<?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' ) ) {
abstract class WC_Connect_Compatibility {
private static $singleton;
private static $version = WC_VERSION;
/**
* @return WC_Connect_Compatibility
*/
public static function instance() {
if ( is_null( self::$singleton ) ) {
self::$singleton = self::select_compatibility();
}
return self::$singleton;
}
/**
* @return WC_Connect_Compatibility subclass for active version of WooCommerce
*/
private static function select_compatibility() {
if ( version_compare( self::$version, '3.0.0', '<' ) ) {
require_once 'class-wc-connect-compatibility-wc26.php';
return new WC_Connect_Compatibility_WC26();
} else {
require_once 'class-wc-connect-compatibility-wc30.php';
return new WC_Connect_Compatibility_WC30();
}
}
public static function set_version( $value ) {
self::$singleton = null;
self::$version = $value;
}
public static function reset_version() {
self::$singleton = null;
self::$version = WC_VERSION;
}
/**
* Get the ID for a given Order.
*
* @param WC_Order $order
*
* @return int
*/
abstract public function get_order_id( WC_Order $order );
/**
* Get admin url for a given order
*
* @param WC_Order $order
*
* @return string
*/
abstract public function get_edit_order_url( WC_Order $order );
/**
* Get the payment method for a given Order.
*
* @param WC_Order $order
*
* @return string
*/
abstract public function get_payment_method( WC_Order $order );
/**
* Retrieve the corresponding Product for the given Order Item.
*
* @param WC_Order $order
* @param WC_Order_Item|WC_Order_Item_Product|array $item
*
* @return WC_Product
*/
abstract public function get_item_product( WC_Order $order, $item );
/**
* Get formatted list of Product Variations, if applicable.
*
* @param WC_Product_Variation $product
* @param bool $flat
*
* @return string
*/
abstract public function get_formatted_variation( WC_Product_Variation $product, $flat = false );
/**
* Get the most specific ID for a given Product.
*
* Note: Returns the Variation ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
abstract public function get_product_id( WC_Product $product );
/**
* Get the top-level ID for a given Product.
*
* Note: Returns the Parent ID for Variable Products.
*
* @param WC_Product $product
*
* @return int
*/
abstract public function get_parent_product_id( WC_Product $product );
/**
* 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
* @return string The product (or variation) name, ready to print
*/
abstract public function get_product_name_from_order( $product_id, $order );
/**
* For a given product, return it's name. In supported versions, variable
* products will include their attributes.
*
* @param WC_Product $product Product (variable, simple, etc)
* @return string The product (or variation) name, ready to print
*/
abstract public function get_product_name( WC_Product $product );
}
}

View File

@@ -0,0 +1,38 @@
<?php
// No direct access please
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Debug_Tools' ) ) {
class WC_Connect_Debug_Tools {
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 Services connection', 'woocommerce-services' ),
'button' => __( 'Test Connection', 'woocommerce-services' ),
'desc' => __( 'This will test your WooCommerce Services connection to ensure everything is working correctly', 'woocommerce-services' ),
'callback' => array( $this, 'test_connection' ),
);
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>' . __( 'Your site is succesfully communicating to the WooCommerce Services API.', 'woocommerce-services' ) . '</p></div>';
} else {
echo '<div class="error inline"><p>' . __( 'ERROR: Your site has a problem connecting to the WooCommerce Services API. Please make sure your Jetpack connection is working.', 'woocommerce-services' ) . '</p></div>';
}
}
}
}

View File

@@ -0,0 +1,94 @@
<?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' );
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 ) && 'product_missing_weight' === $error->get_error_code() ) {
$error_data = $error->get_error_data();
$product_id = $error_data['product_id'];
$product = wc_get_product( $product_id );
if ( ! $product || $product->has_weight() ) {
$this->disable_notice();
return;
}
$product_name = WC_Connect_Compatibility::instance()->get_product_name( $product );
$message = sprintf(
__( '<strong>%2$s does not have a weight defined.</strong><br />Shipping rates cannot be calculated. <a href="%1$s">Add a weight for %2$s</a> so your customers can purchase this item.', 'woocommerce-services' ),
get_edit_post_link( $product_id ), $product_name
);
} else {
$message = sprintf(
__( 'An error occurred in WooCommerce Services. Details are logged <a href="%s">here</a>.', 'woocommerce-services' ),
$link_status
);
}
$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 "";
}
}
}

View File

@@ -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 Services 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;
}
}
}

View File

@@ -0,0 +1,23 @@
<?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() {
return ! (
// Skip for carts loaded from session in the dashboard
( is_admin() && did_action( 'woocommerce_cart_loaded_from_session' ) ) ||
// Skip during Jetpack API requests
( false !== strpos( $_SERVER['REQUEST_URI'], 'jetpack/v4/' ) ) ||
// 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'] ) )
);
}
}
}

View File

@@ -0,0 +1,340 @@
<?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;
/**
* @array
*/
protected $fieldsets;
public function __construct( WC_Connect_Service_Schemas_Store $service_schemas_store,
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;
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 %s or higher is required (You are running %s)', 'woocommerce-services' ),
WOOCOMMERCE_CONNECT_MINIMUM_WOOCOMMERCE_VERSION,
WC()->version
),
);
} else if ( 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;
// Jetpack
// Only one of the following should present
// Check that Jetpack is active
// Check that Jetpack is connected
include_once ( ABSPATH . 'wp-admin/includes/plugin.php' ); // required for is_plugin_active
$is_connected = WC_Connect_Jetpack::is_active() || WC_Connect_Jetpack::is_development_mode();
if ( ! is_plugin_active( 'jetpack/jetpack.php' ) ) {
$health_item = array(
'state' => 'error',
'message' => sprintf(
__( 'Please install and activate the Jetpack plugin, version %s or higher', 'woocommerce-services' ),
WOOCOMMERCE_CONNECT_MINIMUM_JETPACK_VERSION
),
);
} else if ( version_compare( JETPACK__VERSION, WOOCOMMERCE_CONNECT_MINIMUM_JETPACK_VERSION, "<" ) ) {
$health_item = array(
'state' => 'error',
'message' => sprintf(
__( 'Jetpack %s or higher is required (You are running %s)', 'woocommerce-services' ),
WOOCOMMERCE_CONNECT_MINIMUM_JETPACK_VERSION,
JETPACK__VERSION
),
);
} else if ( ! $is_connected ) {
$health_item = array(
'state' => 'error',
'message' => __( 'Jetpack is not connected to WordPress.com. Make sure the Jetpack plugin is installed, activated, and connected.', 'woocommerce-services' ),
);
} else if ( WC_Connect_Jetpack::is_staging_site() ) {
$health_item = array(
'state' => 'warning',
'message' => __( 'This is a Jetpack staging site', 'woocommerce-services' ),
);
} else {
$health_item = array(
'state' => 'success',
'message' => sprintf(
__( 'Jetpack %s is connected and working correctly', 'woocommerce-services' ),
JETPACK__VERSION
),
);
}
$health_items['jetpack'] = $health_item;
// Lastly, do the WooCommerce Services health check
// Check that we have schema
// Check that we are able to talk to the WooCommerce Services server
$schemas = $this->service_schemas_store->get_service_schemas();
$last_fetch_timestamp = $this->service_schemas_store->get_last_fetch_timestamp();
if ( isset( $_GET['refresh'] ) && 'failed' === $_GET['refresh'] ) {
$health_item = array(
'state' => 'error',
'message' => __( 'An error occurred while refreshing service data.', 'woocommerce-services' ),
'timestamp' => $last_fetch_timestamp,
);
} else if ( is_null( $schemas ) ) {
$health_item = array(
'state' => 'error',
'message' => __( 'No service data available', 'woocommerce-services' ),
);
} else if ( is_null( $last_fetch_timestamp ) ) {
$health_item = array(
'state' => 'warning',
'message' => __( 'Service data was found, but may be out of date', 'woocommerce-services' ),
'timestamp' => $last_fetch_timestamp
);
} else if ( $last_fetch_timestamp < time() - WOOCOMMERCE_CONNECT_SCHEMA_AGE_ERROR_THRESHOLD ) {
$health_item = array(
'state' => 'error',
'message' => __( 'Service data was found, but is more than three days old', 'woocommerce-services' ),
'timestamp' => $last_fetch_timestamp
);
} else if ( $last_fetch_timestamp < time() - WOOCOMMERCE_CONNECT_SCHEMA_AGE_WARNING_THRESHOLD ) {
$health_item = array(
'state' => 'warning',
'message' => __( 'Service data was found, but is more than one day old', 'woocommerce-services' ),
'timestamp' => $last_fetch_timestamp
);
} else {
$health_item = array(
'state' => 'success',
'message' => __( 'Service data is up-to-date', 'woocommerce-services' ),
'timestamp' => $last_fetch_timestamp
);
}
$health_items['woocommerce_services'] = $health_item;
return $health_items;
}
protected function get_services_items() {
$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' );
} else if ( -1 === $last_failed_request_timestamp ) {
$state = 'warning';
$message = __( 'No rate requests have yet been made for this service', 'woocommerce-services' );
} else if ( 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 Services 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 Services', 'The WooCommerce Services brandname', 'woocommerce-services' );
return $tabs;
}
/**
* Returns the data bootstrap for the help page
*
* @return array
*/
protected function get_form_data() {
$form_data = 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(),
)
);
return $form_data;
}
/**
* Localizes the bootstrap, enqueues the script and styles for the help page
*/
public function page() {
if ( isset( $_GET['refresh'] ) && 'true' === $_GET['refresh'] ) {
$fetched = $this->service_schemas_store->fetch_service_schemas_from_connect_server();
$url = add_query_arg( 'refresh', $fetched ? false : 'failed' );
wp_safe_redirect( $url );
}
?>
<h2>
<?php _e( 'WooCommerce Services 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(
'storeOptions' => $this->service_settings_store->get_store_options(),
'paperSize' => $this->service_settings_store->get_preferred_paper_size(),
) );
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
if ( ! class_exists( 'WC_Connect_Jetpack' ) ) {
class WC_Connect_Jetpack {
/**
* Helper method to get if Jetpack is in development mode
* @return bool
*/
public static function is_development_mode() {
if ( method_exists( 'Jetpack', 'is_development_mode' ) ) {
return Jetpack::is_development_mode();
}
return false;
}
/**
* Helper method to get if Jetpack is connected (aka active)
* @return bool
*/
public static function is_active() {
if ( method_exists( 'Jetpack', 'is_active' ) ) {
return Jetpack::is_active();
}
return false;
}
/**
* Helper method to get if the current Jetpack website is marked as staging
* @return bool
*/
public static function is_staging_site() {
if ( method_exists( 'Jetpack', 'is_staging_site' ) ) {
return Jetpack::is_staging_site();
}
return false;
}
/**
* Helper method to get whether the current site is an Atomic site
* @return bool
*/
public static function is_atomic_site() {
if ( function_exists( 'jetpack_is_atomic_site' ) ) {
return jetpack_is_atomic_site();
} elseif ( function_exists( 'jetpack_is_automated_transfer_site' ) ) {
return jetpack_is_automated_transfer_site();
}
return false;
}
public static function get_connected_user_data( $user_id ) {
if ( method_exists( 'Jetpack', 'get_connected_user_data' ) ) {
return Jetpack::get_connected_user_data( $user_id );
}
return false;
}
/**
* Helper method to get the Jetpack master user, IF we are connected
* @return WP_User | false
*/
public static function get_master_user() {
include_once ( ABSPATH . 'wp-admin/includes/plugin.php' );
if ( self::is_active() && method_exists( 'Jetpack_Options', 'get_option' ) ) {
$master_user_id = Jetpack_Options::get_option( 'master_user' );
return get_userdata( $master_user_id );
}
return false;
}
/**
* Builds a connect url
* @param $redirect_url
* @return string
*/
public static function build_connect_url( $redirect_url ) {
return Jetpack::init()->build_connect_url(
true,
$redirect_url,
'woocommerce-services-auto-authorize'
);
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
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 date_i18n( 'Y-m-d', current_time( 'timestamp' ) ); ?>.csv"
class="export_csv"
data-export="table"
>
<?php _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;
$query = "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = 'wc_connect_labels'";
$db_results = $wpdb->get_results( $query );
$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->post_id ) );
}
}
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 timestapms
$start_date = $this->start_date * 1000;
$end_date = $this->end_date * 1000;
$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_order_url( $post_id ) {
$order = wc_get_order( $post_id );
return '<a href="' . WC_Connect_Compatibility::instance()->get_edit_order_url( $order ) . '">' . $order->get_order_number( $order ) . '</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 get_date_from_gmt( date( 'Y-m-d H:i:s', $label['created'] / 1000 ) ); ?>
</th>
<td>
<?php echo $this->get_order_url( $label['order_id'] ); ?>
</td>
<td>
<?php echo wc_price( $label['rate'] ); ?>
</td>
<td>
<?php echo $label['service_name']; ?>
</td>
<td>
<?php echo $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 _e( 'Total', 'woocommerce-services' ); ?>
</th>
<th>
<?php echo count( $labels ); ?>
</th>
<th>
<?php echo 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
}
}
}

View File

@@ -0,0 +1,135 @@
<?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_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 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 );
}
}
}

View File

@@ -0,0 +1,730 @@
<?php
if ( ! class_exists( 'WC_Connect_Nux' ) ) {
class WC_Connect_Nux {
/**
* Jetpack status constants.
*/
const JETPACK_NOT_INSTALLED = 'uninstalled';
const JETPACK_INSTALLED_NOT_ACTIVATED = 'installed';
const JETPACK_ACTIVATED_NOT_CONNECTED = 'activated';
const JETPACK_DEV = 'dev';
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_woocommerce_page_wc-settings', array( $this, 'register_add_service_to_zone_pointer' ) );
add_filter( 'wc_services_pointer_post.php', array( $this, 'register_order_page_labels_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();
}
public function register_add_service_to_zone_pointer( $pointers ) {
$pointers[] = array(
'id' => 'wc_services_add_service_to_zone',
'target' => 'th.wc-shipping-zone-methods',
'options' => array(
'content' => sprintf( '<h3>%s</h3><p>%s</p>',
__( 'Add a WooCommerce shipping service to a Zone' ,'woocommerce-services' ),
__( "To ship products to customers using USPS or Canada Post, you will need to add them as a shipping method to an applicable zone. If you don't have any zones, add one first.", 'woocommerce-services' )
),
'position' => array( 'edge' => 'right', 'align' => 'left' ),
),
);
return $pointers;
}
public function is_new_labels_user() {
$is_new_user = get_transient( self::IS_NEW_LABEL_USER );
if ( false === $is_new_user ) {
global $wpdb;
$query = "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key = 'wc_connect_labels' LIMIT 1";
$results = $wpdb->get_results( $query );
$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 ) {
$dismissed_pointers = $this->get_dismissed_pointers();
if ( in_array( 'wc_services_labels_metabox', $dismissed_pointers, true ) ) {
return $pointers;
}
// If the user is not new to labels, we should just dismiss this pointer
if ( ! $this->is_new_labels_user() ) {
$dismissed_pointers[] = 'wc_services_labels_metabox';
$dismissed_data = implode( ',', $dismissed_pointers );
update_user_meta( get_current_user_id(), 'dismissed_wp_pointers', $dismissed_data );
return $pointers;
}
if ( $this->shipping_label->should_show_meta_box() ) {
$pointers[] = array(
'id' => 'wc_services_labels_metabox',
'target' => '#woocommerce-order-label',
'options' => array(
'content' => sprintf( '<h3>%s</h3><p>%s</p>',
__( 'Discounted Shipping Labels' ,'woocommerce-services' ),
__( "When you're ready, purchase and print discounted labels from USPS right here.", 'woocommerce-services' )
),
'position' => array( 'edge' => 'right', 'align' => 'left' ),
),
'dim' => true,
);
}
return $pointers;
}
/**
* Check that the current user is the owner of the Jetpack connection
* - Only that person can accept the TOS
*
* @uses self::get_jetpack_install_status()
*
* @return bool
*/
public function can_accept_tos() {
$jetpack_status = $this->get_jetpack_install_status();
if (
( self::JETPACK_NOT_INSTALLED === $jetpack_status ) ||
( self::JETPACK_INSTALLED_NOT_ACTIVATED === $jetpack_status )
) {
return false;
}
// Developer case
if ( self::JETPACK_DEV === $jetpack_status ) {
return true;
}
$user_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
$can_accept = (
isset( $user_token->external_user_id ) &&
get_current_user_id() === $user_token->external_user_id
);
return $can_accept;
}
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_INSTALLED:
case self::JETPACK_INSTALLED_NOT_ACTIVATED:
case self::JETPACK_ACTIVATED_NOT_CONNECTED:
return 'before_jetpack_connection';
case self::JETPACK_CONNECTED:
case self::JETPACK_DEV:
// 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() {
// we need to use validate_plugin to check that Jetpack is installed
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
// check if jetpack is installed
if ( 0 !== validate_plugin( 'jetpack/jetpack.php' ) ) {
return self::JETPACK_NOT_INSTALLED;
}
// check if Jetpack is activated
if ( ! class_exists( 'Jetpack_Data' ) ) {
return self::JETPACK_INSTALLED_NOT_ACTIVATED;
}
if ( defined( 'JETPACK_DEV_DEBUG' ) && true === JETPACK_DEV_DEBUG ) {
// installed, activated, and dev mode on
return self::JETPACK_DEV;
}
// installed, activated, dev mode off
// check if connected
$user_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
if ( ! isset( $user_token->external_user_id ) ) { // always an int
return self::JETPACK_ACTIVATED_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.
'shop_order' === $screen->post_type
&& 'edit' === $screen->base
)
|| ( // Edit order page.
'shop_order' === $screen->post_type
&& 'post' === $screen->base
)
|| ( // 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://stripe.com/global
*/
public function is_stripe_supported_country( $country_code ) {
$stripe_supported_countries = array(
'AU',
'AT',
'BE',
'CA',
'DK',
'FI',
'FR',
'DE',
'HK',
'IE',
'JP',
'LU',
'NL',
'NZ',
'NO',
'SG',
'ES',
'SE',
'CH',
'GB',
'US',
);
return in_array( $country_code, $stripe_supported_countries );
}
/**
* 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_stripe = $this->is_stripe_supported_country( $store_country );
$supports_taxes = $this->is_taxjar_supported_country( $store_country );
$supports_shipping = in_array( $store_country, array( 'US', 'CA' ) );
return $supports_shipping || $supports_stripe || $supports_taxes;
}
public function get_feature_list_for_country( $country ) {
$feature_list = false;
$supports_stripe = $this->is_stripe_supported_country( $country );
$supports_taxes = $this->is_taxjar_supported_country( $country );
$supports_rates = in_array( $country, array( 'US', 'CA' ) );
$supports_labels = ( 'US' === $country );
$is_stripe_active = is_plugin_active( 'woocommerce-gateway-stripe/woocommerce-gateway-stripe.php' );
$stripe_settings = get_option( 'woocommerce_stripe_settings', array() );
$is_stripe_ready = $is_stripe_active && isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled'];
$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() );
$is_ppec_ready = $is_ppec_active && ( ! isset( $ppec_settings['enabled'] ) || 'yes' === $ppec_settings['enabled'] );
$supports_payments = ( $supports_stripe && $is_stripe_ready ) || $is_ppec_ready;
if ( $supports_payments && $supports_taxes && $supports_rates && $supports_labels ) {
$feature_list = __( 'automated tax calculation, live shipping rates, shipping label printing, and smoother payment setup', 'woocommerce-services' );
} elseif ( $supports_payments && $supports_taxes && $supports_rates ) {
$feature_list = __( 'automated tax calculation, live shipping rates, and smoother payment setup', 'woocommerce-services' );
} else if ( $supports_payments && $supports_taxes ) {
$feature_list = __( 'automated tax calculation and smoother payment setup', 'woocommerce-services' );
} else if ( $supports_payments && $supports_rates && $supports_labels ) {
$feature_list = __( 'live shipping rates, shipping label printing, and smoother payment setup', 'woocommerce-services' );
} else if ( $supports_payments && $supports_rates ) {
$feature_list = __( 'live shipping rates and smoother payment setup', 'woocommerce-services' );
} else if ( $supports_payments ) {
$feature_list = __( 'smoother payment setup', 'woocommerce-services' );
} else if ( $supports_taxes && $supports_rates && $supports_labels ) {
$feature_list = __( 'automated tax calculation, live shipping rates, and shipping label printing', 'woocommerce-services' );
} else if ( $supports_taxes && $supports_rates ) {
$feature_list = __( 'automated tax calculation and live shipping rates', 'woocommerce-services' );
} else if ( $supports_taxes ) {
$feature_list = __( 'automated tax calculation', 'woocommerce-services' );
} else if ( $supports_rates && $supports_labels ) {
$feature_list = __( 'live shipping rates and shipping label printing', 'woocommerce-services' );
} else if ( $supports_rates ) {
$feature_list = __( 'live shipping rates', '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 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();
if ( ( self::JETPACK_NOT_INSTALLED === $jetpack_install_status && ! current_user_can( 'install_plugins' ) )
|| ( self::JETPACK_INSTALLED_NOT_ACTIVATED === $jetpack_install_status && ! current_user_can( 'activate_plugins' ) ) ) {
return;
}
$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' => $this->can_accept_tos(),
'should_display_after_cxn_banner' => WC_Connect_Options::get_option( self::SHOULD_SHOW_AFTER_CXN_BANNER ),
) );
switch ( $banner_to_display ) {
case 'before_jetpack_connection':
$ajax_data = array(
'nonce' => wp_create_nonce( 'wcs_nux_notice' ),
'initial_install_status' => $jetpack_install_status,
'redirect_url' => $this->get_jetpack_redirect_url(),
'translations' => array(
'activating' => __( 'Activating...', 'woocommerce-services' ),
'connecting' => __( 'Connecting...', 'woocommerce-services' ),
'installError' => __( 'There was an error installing Jetpack. Please try installing it manually.', 'woocommerce-services' ),
'defaultError' => __( 'Something went wrong. Please try connecting to Jetpack manually, or contact support on the WordPress.org forums.', 'woocommerce-services' ),
),
);
wp_enqueue_script( 'wc_connect_banner' );
wp_localize_script( 'wc_connect_banner', 'wcs_nux_notice', $ajax_data );
add_action( 'wp_ajax_woocommerce_services_activate_jetpack',
array( $this, 'ajax_activate_jetpack' )
);
add_action( 'wp_ajax_woocommerce_services_get_jetpack_connect_url',
array( $this, 'ajax_get_jetpack_connect_url' )
);
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 );
$jetpack_status = $this->get_jetpack_install_status();
$button_text = __( 'Connect', 'woocommerce-services' );
$banner_title = __( 'Connect Jetpack to activate WooCommerce Services', 'woocommerce-services' );
$image_url = plugins_url( 'images/wcs-notice.png', dirname( __FILE__ ) );
switch ( $jetpack_status ) {
case self::JETPACK_NOT_INSTALLED:
$button_text = __( 'Install Jetpack and connect', 'woocommerce-services' );
break;
case self::JETPACK_INSTALLED_NOT_ACTIVATED:
$button_text = __( 'Activate Jetpack and connect', 'woocommerce-services' );
break;
}
$country = WC()->countries->get_base_country();
/* translators: %s: list of features, potentially comma separated */
$description_base = __( "WooCommerce Services is almost ready to go! Once you connect Jetpack you'll have access to %s.", 'woocommerce-services' );
$feature_list = $this->get_feature_list_for_country( $country );
$banner_content = array(
'title' => $banner_title,
'description' => sprintf( $description_base, $feature_list ),
'button_text' => $button_text,
'image_url' => $image_url,
'should_show_jp' => true,
'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_jp' => false,
'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 Services is almost ready to go! Once you connect your store 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 store to activate WooCommerce Services', '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_jp' => false,
'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 if ( $content['should_show_jp'] ) : ?>
<img
class="wcs-nux__notice-logo-jetpack"
src="<?php echo esc_url( plugins_url( 'images/jetpack-logo.png', dirname( __FILE__ ) ) ); ?>"
>
<?php endif; ?>
<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 $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 the <a href="%2$s">Terms of Service</a> and to <a href="%3$s">share certain data and settings</a> with WordPress.com and/or third parties.', 'woocommerce-services' ),
array(
'a' => array(
'href' => array(),
),
) ),
esc_html( $content['button_text'] ),
'https://wordpress.com/tos/',
'https://jetpack.com/support/what-data-does-jetpack-sync/'
); ?></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 : ?>
<button
class="woocommerce-services__connect-jetpack wcs-nux__notice-content-button button button-primary"
>
<?php echo esc_html( $content['button_text'] ); ?>
</button>
<?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;
}
/**
* Activates Jetpack after an ajax request
*/
public function ajax_activate_jetpack() {
check_ajax_referer( 'wcs_nux_notice' );
$result = activate_plugin( 'jetpack/jetpack.php' );
if ( is_null( $result ) ) {
// The function activate_plugin() returns NULL on success.
echo 'success';
} else {
if ( is_wp_error( $result ) ) {
echo esc_html( $result->get_error_message() );
} else {
echo 'error';
}
}
wp_die();
}
/**
* Get Jetpack connection URL.
*
*/
public function ajax_get_jetpack_connect_url() {
check_ajax_referer( 'wcs_nux_notice' );
$redirect_url = '';
if ( isset( $_POST['redirect_url'] ) ) {
$redirect_url = esc_url_raw( wp_unslash( $_POST['redirect_url'] ) );
}
$connect_url = WC_Connect_Jetpack::build_connect_url( $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 );
echo esc_url_raw( $connect_url );
wp_die();
}
}
}

View File

@@ -0,0 +1,338 @@
<?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',
);
case 'shipping_method':
return array(
'form_settings',
'failure_timestamp',
);
}
return array(
'tos_accepted',
'store_guid',
'debug_logging_enabled',
'debug_display_enabled',
'payment_methods',
'account_settings',
'paper_size',
'packages',
'predefined_packages',
'shipping_methods_migrated',
'should_display_nux_after_jp_cxn_banner',
'needs_tax_environment_setup',
'stripe_state',
'banner_ppec',
);
}
/**
* Deletes all options created by WooCommerce Services, 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( sprintf( 'Invalid WooCommerce Services 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( sprintf( 'Invalid WooCommerce Services 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( sprintf( 'Invalid WooCommerce Services 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( sprintf( 'Invalid WooCommerce Services 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( sprintf( 'Invalid WooCommerce Services 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( sprintf( 'Invalid WooCommerce Services 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 );
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,119 @@
<?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;
}
$payment_methods = $this->get_payment_methods_from_response_body( $response_body );
if ( is_wp_error( $payment_methods ) ) {
$this->logger->log( $payment_methods, __FUNCTION__ );
return false;
}
// If we made it this far, it is safe to store the object
$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? Select the first available one
$selected_payment_method_id = $this->service_settings_store->get_selected_payment_method_id();
if (
$selected_payment_method_id &&
! 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 );
}
protected function get_payment_methods_from_response_body( $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.' );
}
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.' );
}
$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 ( (array) $payment_methods as $payment_method ) {
$required_keys = array( 'payment_method_id', 'name', 'card_type', 'card_digits', 'expiry' );
foreach ( (array) $required_keys as $required_key ) {
if ( ! array_key_exists( $required_key, $payment_method ) ) {
return new WP_Error( 'payment_methods_key_missing', 'Payment method array element is missing a required key' );
}
}
}
return $payment_methods;
}
}
}

View File

@@ -0,0 +1,342 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_PayPal_EC' ) ) {
/**
* Integrates with WooCommerce PayPal Express 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 Express 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 = WC_Connect_Compatibility::instance()->get_payment_method( $order );
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();
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' === WC_Connect_Compatibility::instance()->get_payment_method( wc_get_order() )
)
|| ( // WooCommerce settings.
'woocommerce_page_wc-settings' === $screen->base
&& isset( $_GET['tab'] ) && 'checkout' === $_GET['tab']
)
|| ( // WooCommerce payment gateway extension page
'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 Services 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, 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, 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 Services, <a href="%s">click here</a>.', 'woocommerce-services' );
if ( empty( $settings->api_username ) ) {
$api_creds_text = sprintf( $api_creds_template, 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, 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' )
) {
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;
}
}
}

View File

@@ -0,0 +1,118 @@
<?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 Services', '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 Services 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 object $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 );
update_post_meta( $order_id, 'wc_connect_labels', $labels );
}
}

View File

@@ -0,0 +1,250 @@
<?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 ) ) {
$this->logger->log( $response_body, __FUNCTION__ );
return false;
}
$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() );
}
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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,593 @@
<?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;
}
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 get_origin_address() {
$wc_address_fields = array();
$wc_address_fields['company'] = get_bloginfo( 'name' );
$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() );
return array_merge( $wc_address_fields, $stored_address_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 ) {
$label_data = get_post_meta( ( int ) $order_id, '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;
$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'] );
}
}
}
update_post_meta( $order_id, 'wc_connect_labels', $labels_data );
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 );
update_post_meta( $order_id, 'wc_connect_labels', $labels_data );
}
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'] );
$order->set_address( $new_address, 'shipping' );
update_post_meta( $order_id, '_wc_connect_destination_normalized', true );
}
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 Services 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;
$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;"
);
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;
} else if ( 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
|| ! $this->service_schemas_store->fetch_service_schemas_from_connect_server() ) { //ensure the latest schemas are fetched
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 ) {
wp_send_json_error(
array(
'error' => 'bad_instance_id',
'message' => __( '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 ) {
wp_send_json_error(
array(
'error' => 'bad_service_id',
'message' => __( '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
wp_send_json_error(
array(
'error' => 'validation_failure',
'message' => $response_body->get_error_message(),
'data' => $response_body->get_error_data(),
)
);
}
// 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() );
}
/**
* 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 ];
}
/**
* 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;
}
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;
}
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
if ( ! class_exists( 'WC_Connect_Settings_Pages' ) ) {
class WC_Connect_Settings_Pages {
/**
* @array
*/
protected $fieldsets;
public function __construct() {
$this->id = 'connect';
$this->label = _x( 'WooCommerce Services', 'The WooCommerce Services brandname', 'woocommerce-services' );
add_filter( 'woocommerce_get_sections_shipping', array( $this, 'get_sections' ), 30 );
add_action( 'woocommerce_settings_shipping', array( $this, 'output_settings_screen' ) );
}
/**
* Get sections.
*
* @return array
*/
public function get_sections( $shipping_tabs ) {
if ( ! is_array( $shipping_tabs ) ) {
$shipping_tabs = array();
}
$shipping_tabs[ 'woocommerce-services-settings' ] = __( 'WooCommerce Services', 'woocommerce-services' );
return $shipping_tabs;
}
/**
* Output the settings.
*/
public function output_settings_screen() {
global $current_section;
if ( 'woocommerce-services-settings' !== $current_section ) {
return;
}
$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_development_mode() ) {
if ( WC_Connect_Jetpack::is_active() ) {
$message = __( 'Note: Jetpack is connected, but development mode is also enabled on this site. Please disable development mode.', 'woocommerce-services' );
} else {
$message = __( 'Note: Jetpack development mode is enabled on this site. This site will not be able to obtain payment methods from WooCommerce Services production servers.', 'woocommerce-services' );
}
?>
<div class="wc-connect-admin-dev-notice">
<p>
<?php echo esc_html( $message ); ?>
</p>
</div>
<?php
}
$extra_args = array();
if ( isset( $_GET['from_order'] ) ) {
$extra_args['order_id'] = $_GET['from_order'];
$extra_args['order_href'] = get_edit_post_link( $_GET['from_order'] );
}
do_action( 'enqueue_wc_connect_script', 'wc-connect-shipping-settings', $extra_args );
}
}
}

View File

@@ -0,0 +1,468 @@
<?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_Payment_Methods_Store
*/
protected $payment_methods_store;
/**
* @var array Supported countries
*/
private $supported_countries = array( 'US', 'PR' );
/**
* @var array Supported currencies
*/
private $supported_currencies = array( 'USD' );
/**
* @var array Unsupported states, by country
*/
private $unsupported_states = array(
'US' => array( 'AA', 'AE', 'AP' ),
);
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
) {
$this->api_client = $api_client;
$this->settings_store = $settings_store;
$this->service_schemas_store = $service_schemas_store;
}
public function get_item_data( WC_Order $order, $item ) {
$product = WC_Connect_Compatibility::instance()->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();
}
$product_data = array(
'height' => (float) $height,
'product_id' => $item['product_id'],
'length' => (float) $length,
'quantity' => 1,
'weight' => (float) $weight,
'width' => (float) $width,
'name' => $this->get_name( $product ),
'url' => get_edit_post_link( WC_Connect_Compatibility::instance()->get_parent_product_id( $product ), null ),
);
if ( $product->is_type( 'variation' ) ) {
$product_data['attributes'] = WC_Connect_Compatibility::instance()->get_formatted_variation( $product, true );
}
return $product_data;
}
public function get_items_as_individual_packages( WC_Order $order ) {
$packages = array();
$item_count = 0;
foreach ( $order->get_items() as $item ) {
$item_data = $this->get_item_data( $order, $item );
if ( null === $item_data ) {
continue;
}
for ( $i = 0; $i < $item['qty']; $i++ ) {
$id = 'weight_' . $item_count++ . '_individual';
$packages[ $id ] = array(
'id' => $id,
'box_id' => 'individual',
'height' => $item_data['height'],
'length' => $item_data['length'],
'weight' => $item_data['weight'],
'width' => $item_data['width'],
'items' => array( $item_data ),
);
}
}
return $packages;
}
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 = '#' . WC_Connect_Compatibility::instance()->get_product_id( $product );
}
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' ) );
return 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_Compatibility::instance()->get_item_product( $order, $product_data );
if ( $product ) {
$product_data['name'] = $this->get_name( $product );
$product_data['url'] = get_edit_post_link( WC_Connect_Compatibility::instance()->get_parent_product_id( $product ), null );
if ( $product->is_type( 'variation' ) ) {
$formatted = WC_Connect_Compatibility::instance()->get_formatted_variation( $product, true );
$product_data['attributes'] = $formatted;
}
} else {
$product_data['name'] = WC_Connect_Compatibility::instance()->get_product_name_from_order( $product_data['product_id'], $order );
}
$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;
}
for ( $i = 0; $i < $item['qty']; $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 = WC_Connect_Compatibility::instance()->get_order_id( $order );
$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 ) get_post_meta( $order_id, '_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;
}
private function is_supported_state( $country_code, $state_code ) {
if ( ! $country_code || ! $state_code ) {
return true;
}
if ( ! array_key_exists( $country_code, $this->unsupported_states ) ) {
return true;
}
return ! in_array( $state_code, $this->unsupported_states[ $country_code ] );
}
private function is_supported_country( $country_code ) {
return in_array( $country_code, $this->supported_countries );
}
private function is_supported_currency( $currency_code ) {
return in_array( $currency_code, $this->supported_currencies );
}
private function is_supported_address( $address ) {
$country_code = $address['country'];
if ( ! $country_code ) {
return true;
}
if ( ! $this->is_supported_country( $country_code ) ) {
return false;
}
$state_code = $address['state'];
return $this->is_supported_state( $country_code, $state_code );
}
protected function get_states_map() {
$result = array();
$all_countries = WC()->countries->get_countries();
foreach ( $this->supported_countries as $country_code ) {
$country_data = array( 'name' => html_entity_decode( $all_countries[ $country_code ] ) );
$states = WC()->countries->get_states( $country_code );
if ( $states ) {
$country_data['states'] = array();
foreach ( $states as $state_code => $name ) {
if ( ! $this->is_supported_state( $country_code, $state_code ) ) {
continue;
}
$country_data['states'][ $state_code ] = html_entity_decode( $name );
}
}
$result[ $country_code ] = $country_data;
}
return $result;
}
public function should_show_meta_box() {
if ( null === $this->show_metabox ) {
$this->show_metabox = $this->calculate_should_show_meta_box();
}
return $this->show_metabox;
}
private function calculate_should_show_meta_box() {
$order = wc_get_order();
if ( ! $order ) {
return false;
}
// If the order already has purchased labels, show the meta-box no matter what
if ( get_post_meta( WC_Connect_Compatibility::instance()->get_order_id( $order ), 'wc_connect_labels', true ) ) {
return true;
}
// Restrict showing the metabox to supported store currencies.
$base_currency = get_woocommerce_currency();
if ( ! $this->is_supported_currency( $base_currency ) ) {
return false;
}
// Restrict showing the meta-box to supported origin and destinations: US domestic, for now
$base_location = wc_get_base_location();
if ( ! $this->is_supported_country( $base_location['country'] ) ) {
return false;
}
$dest_address = $order->get_address( 'shipping' );
if ( ! $this->is_supported_address( $dest_address ) ) {
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_Compatibility::instance()->get_item_product( $order, $item );
if ( $product && $product->needs_shipping() ) {
return true;
}
}
return false;
}
public function get_label_payload( $post_order_or_id ) {
$order = wc_get_order( $post_order_or_id );
if ( ! $order ) {
return false;
}
$account_settings = $this->settings_store->get_account_settings();
$order_id = WC_Connect_Compatibility::instance()->get_order_id( $order );
$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 ),
//for backwards compatibility, still disable the country dropdown for calypso users with older plugin versions
'canChangeCountries' => true,
);
$store_options = $this->settings_store->get_store_options();
$store_options['countriesData'] = $this->get_states_map();
$payload['storeOptions'] = $store_options;
return $payload;
}
public function meta_box( $post ) {
$order = wc_get_order( $post );
$order_id = WC_Connect_Compatibility::instance()->get_order_id( $order );
$payload = array(
'orderId' => $order_id,
);
do_action( 'enqueue_wc_connect_script', 'wc-connect-create-shipping-label', $payload );
}
}
}

View File

@@ -0,0 +1,542 @@
<?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;
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
}
}
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
* @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'] : '';
// Ensure that Country is specified
if ( empty( $country ) ) {
$this->debug( 'Skipping rate calculation - missing country', 'error' );
return false;
}
// Validate Postcode
if ( ! WC_Validation::is_postcode( $postcode, $country ) ) {
$this->debug( 'Skipping rate calculation - invalid postcode', 'error' );
return false;
}
// Validate State
$valid_states = WC()->countries->get_states( $country );
if ( $valid_states && ! array_key_exists( $state, $valid_states ) ) {
$this->debug( 'Skipping rate calculation - invalid/unsupported state', 'error' );
return false;
}
return true;
}
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 Services 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 ) ) {
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 Services 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 ) ) )
);
$debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' );
$response_body = wp_cache_get( $cache_key );
if ( ! $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;
}
wp_cache_set( $cache_key, $response_body, '', 3600 );
}
$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 ) {
/** @var WC_Product $product */
$product = $this->lookup_product( $package, $product_id );
if ( $product ) {
$item_name = WC_Connect_Compatibility::instance()->get_product_name( $product );
$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>%s x</em> ', $count ) : '' ) .
sprintf( '<strong>%s</strong> %s', $item_name, $item_measurements );
}
}
$package_measurements = '';
if ( ! property_exists( $rate_package, 'box_id' ) ) {
$package_name = __( 'Unknown package 📦', 'woocommerce-services' );
} else if ( 'individual' === $rate_package->box_id ) {
$package_name = __( 'Individual packaging 📦', 'woocommerce-services' );
} else if (
isset( $packaging_lookup[ $rate_package->box_id ] ) &&
isset( $packaging_lookup[ $rate_package->box_id ]['name'] )
) {
$is_letter = isset( $packaging_lookup[ $rate_package->box_id ]['is_letter'] ) && $packaging_lookup[ $rate_package->box_id ]['is_letter'];
$icon = $is_letter ? '✉️' : '📦';
$package_name = $packaging_lookup[ $rate_package->box_id ]['name'] . ' ' . $icon;
$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 ) );
$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
),
);
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 {
$this->debug(
sprintf(
'Received rate: <strong>%s</strong> (%s)<br/><ul><li>%s</li></ul>',
$rate_to_add['label'],
wc_price( $rate->rate ),
implode( '</li><li>', $package_summaries )
),
'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' ) {
if ( is_cart() || is_checkout() || isset( $_POST['update_cart'] ) ) {
$debug_message = sprintf( '%s (%s:%d)', $message, esc_html( $this->title ), $this->instance_id );
$this->logger->debug( $debug_message, $type );
}
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WC_Connect_Stripe' ) ) {
class WC_Connect_Stripe {
/**
* @var WC_Connect_API_Client
*/
private $api;
/**
* @var WC_Connect_Options
*/
private $options;
/**
* @var WC_Connect_Logger
*/
private $logger;
const STATE_VAR_NAME = 'stripe_state';
const SETTINGS_OPTION = 'woocommerce_stripe_settings';
public function __construct( WC_Connect_API_Client $client, WC_Connect_Options $options, WC_Connect_Logger $logger ) {
$this->api = $client;
$this->options = $options;
$this->logger = $logger;
}
public function is_stripe_plugin_enabled() {
return class_exists( 'WC_Stripe' );
}
public function get_oauth_url( $return_url ) {
$result = $this->api->get_stripe_oauth_init( $return_url );
if ( is_wp_error( $result ) ) {
return $result;
}
$this->options->update_option( self::STATE_VAR_NAME, $result->state );
return $result->oauthUrl;
}
public function create_account( $email, $country ) {
$response = $this->api->create_stripe_account( $email, $country );
if ( is_wp_error( $response ) ) {
return $response;
}
return $this->save_stripe_keys( $response );
}
public function get_account_details() {
return $this->api->get_stripe_account_details();
}
public function deauthorize_account() {
$response = $this->api->deauthorize_stripe_account();
if ( is_wp_error( $response ) ) {
return $response;
}
$this->clear_stripe_keys();
return $response;
}
public function connect_oauth( $state, $code ) {
if ( $state !== $this->options->get_option( self::STATE_VAR_NAME, false ) ) {
return new WP_Error( 'Invalid stripe state' );
}
$response = $this->api->get_stripe_oauth_keys( $code );
if ( is_wp_error( $response ) ) {
return $response;
}
return $this->save_stripe_keys( $response );
}
private function save_stripe_keys( $result ) {
if ( ! isset( $result->publishableKey, $result->secretKey ) ) {
return new WP_Error( 'Invalid credentials received from server' );
}
$is_test = false !== strpos( $result->publishableKey, '_test_' );
$prefix = $is_test ? 'test_' : '';
$default_options = $this->get_default_stripe_config();
$options = array_merge( $default_options, get_option( self::SETTINGS_OPTION, array() ) );
$options['enabled'] = 'yes';
$options['testmode'] = $is_test ? 'yes' : 'no';
$options[ $prefix . 'publishable_key' ] = $result->publishableKey;
$options[ $prefix . 'secret_key' ] = $result->secretKey;
// While we are at it, let's also clear the account_id and
// test_account_id if present
// Those used to be stored by save_stripe_keys but should not have
// been since they were not used by anyone
unset( $options[ 'account_id' ] );
unset( $options[ 'test_account_id' ] );
update_option( self::SETTINGS_OPTION, $options );
return $result;
}
/**
* Clears keys for test or production (whichever is presently enabled).
* Especially useful after Stripe Connect account deauthorization.
*/
private function clear_stripe_keys() {
$default_options = $this->get_default_stripe_config();
$options = array_merge( $default_options, get_option( self::SETTINGS_OPTION, array() ) );
if ( 'yes' === $options['testmode'] ) {
$options[ 'test_publishable_key' ] = '';
$options[ 'test_secret_key' ] = '';
} else {
$options[ 'publishable_key' ] = '';
$options[ 'secret_key' ] = '';
}
// While we are at it, let's also clear the account_id and
// test_account_id if present
// Those used to be stored by save_stripe_keys but should not have
// been since they were not used by anyone
unset( $options[ 'account_id' ] );
unset( $options[ 'test_account_id' ] );
update_option( self::SETTINGS_OPTION, $options );
}
private function get_default_stripe_config() {
if ( ! class_exists( 'WC_Gateway_Stripe' ) ) {
return array();
}
$result = array();
$gateway = new WC_Gateway_Stripe();
foreach ( $gateway->form_fields as $key => $value ) {
if ( isset( $value['default'] ) ) {
$result[ $key ] = $value['default'];
}
}
return $result;
}
}
}

View File

@@ -0,0 +1,904 @@
<?php
class WC_Connect_TaxJar_Integration {
/**
* @var WC_Connect_API_Client
*/
public $api_client;
/**
* @var WC_Connect_Logger
*/
public $logger;
private $expected_options = array(
// If automated taxes are enabled and user disables taxes we re-enable them
'woocommerce_calc_taxes' => 'yes',
// Users can set either billing or shipping address for tax rates but not shop
'woocommerce_tax_based_on' => 'shipping',
// Rate calculations assume tax not included
'woocommerce_prices_include_tax' => 'no',
// Use no special handling on shipping taxes, our API handles that
'woocommerce_shipping_tax_class' => '',
// API handles rounding precision
'woocommerce_tax_round_at_subtotal' => 'no',
// Rates are calculated in the cart assuming tax not included
'woocommerce_tax_display_shop' => 'excl',
// TaxJar returns one total amount, not line item amounts
'woocommerce_tax_display_cart' => 'excl',
// TaxJar returns one total amount, not line item amounts
'woocommerce_tax_total_display' => 'single',
);
const PROXY_PATH = 'taxjar/v2';
const OPTION_NAME = 'wc_connect_taxes_enabled';
const SETUP_WIZARD_OPTION_NAME = 'woocommerce_setup_automated_taxes';
public function __construct(
WC_Connect_API_Client $api_client,
WC_Connect_Logger $logger
) {
$this->api_client = $api_client;
$this->logger = $logger;
}
public function init() {
// Only enable WCS TaxJar integration if the official TaxJar plugin isn't active.
if ( class_exists( 'WC_Taxjar' ) ) {
return;
}
$store_settings = $this->get_store_settings();
$store_country = $store_settings['store_country_setting'];
// TaxJar supports USA, Canada, Australia, and the European Union
if ( ! $this->is_supported_country( $store_country ) ) {
return;
}
// Add toggle for automated taxes to the core settings page
add_filter( 'woocommerce_tax_settings', array( $this, 'add_tax_settings' ) );
// Settings values filter to handle the hardcoded settings
add_filter( 'woocommerce_admin_settings_sanitize_option', array( $this, 'sanitize_tax_option' ), 10, 2 );
// Settings Page
add_action( 'woocommerce_sections_tax', array( $this, 'output_sections_before' ), 9 );
add_action( 'woocommerce_sections_tax', array( $this, 'output_sections_after' ), 11 );
// Bow out if we're not wanted
if ( ! $this->is_enabled() ) {
return;
}
$this->configure_tax_settings();
// Calculate Taxes at Cart / Checkout
if ( class_exists( 'WC_Cart_Totals' ) ) { // Woo 3.2+
add_action( 'woocommerce_after_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
} else {
add_action( 'woocommerce_calculate_totals', array( $this, 'maybe_calculate_totals' ), 20 );
}
// Calculate Taxes for Backend Orders (Woo 2.6+)
add_action( 'woocommerce_before_save_order_items', array( $this, 'calculate_backend_totals' ), 20 );
// Set customer taxable location for local pickup
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'append_base_address_to_customer_taxable_address' ), 10, 1 );
}
/**
* Are automated taxes enabled?
*
* @return bool
*/
public function is_enabled() {
// Migrate automated taxes selection from the setup wizard
if ( get_option( self::SETUP_WIZARD_OPTION_NAME ) ) {
update_option( self::OPTION_NAME, 'yes' );
delete_option( self::SETUP_WIZARD_OPTION_NAME );
return true;
}
return ( wc_tax_enabled() && 'yes' === get_option( self::OPTION_NAME ) );
}
/**
* Add our "automated taxes" setting to the core group.
*
* @param array $tax_settings WooCommerce Tax Settings
*
* @return array
*/
public function add_tax_settings( $tax_settings ) {
$enabled = $this->is_enabled();
$automated_taxes = array(
'title' => __( 'Automated taxes', 'woocommerce-services' ),
'id' => self::OPTION_NAME, // TODO: save in `wc_connect_options`?
'desc_tip' => __( 'Automate your sales tax calculations with WooCommerce Services, powered by Jetpack.', 'woocommerce-services' ),
'desc' => $enabled ? '<p>' . __( 'Powered by WooCommerce Services ― Your tax rates and settings are automatically configured.', 'woocommerce-services' ) . '</p>' : '',
'default' => 'no',
'type' => 'select',
'class' => 'wc-enhanced-select',
'options' => array(
'no' => __( 'Disable automated taxes', 'woocommerce-services' ),
'yes' => __( 'Enable automated taxes', 'woocommerce-services' ),
),
);
// Insert the "automated taxes" setting at the top (under the section title)
array_splice( $tax_settings, 1, 0, array( $automated_taxes ) );
if ( $enabled ) {
// If the automated taxes are enabled, disable the settings that would be reverted in the original plugin
foreach ( $tax_settings as $index => $tax_setting ) {
if ( ! array_key_exists( $tax_setting['id'], $this->expected_options ) ) {
continue;
}
$tax_settings[$index]['custom_attributes'] = array( 'disabled' => true );
}
}
return $tax_settings;
}
/**
* When automated taxes are enabled, overwrite core tax settings that might break the API integration
* This is similar to the original plugin functionality where these options were reverted on page load
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L66-L91
*
* @param mixed $value - option value
* @param array $option - option metadata
* @return string new option value, based on the automated taxes state or $value
*/
public function sanitize_tax_option( $value, $option ) {
if (
//skip unrecognized option format
! is_array( $option )
//skip if unexpected option format
|| ! isset( $option['id'] )
//skip if not enabled or not being enabled in the current request
|| ! $this->is_enabled() && ( ! isset( $_POST[self::OPTION_NAME] ) || 'yes' != $_POST[self::OPTION_NAME] ) ) {
return $value;
}
//the option is currently being enabled - backup the rates and flush the rates table
if ( ! $this->is_enabled() && self::OPTION_NAME === $option['id'] && 'yes' === $value ) {
$this->backup_existing_tax_rates();
return $value;
}
//skip if unexpected option
if ( ! array_key_exists( $option['id'], $this->expected_options ) ) {
return $value;
}
return $this->expected_options[ $option['id'] ];
}
/**
* Overwrite WooCommerce core tax settings if they are different than expected
*
* Ported from TaxJar's plugin and modified to support $this->expected_options
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L66-L91
*/
public function configure_tax_settings() {
foreach( $this->expected_options as $option => $value ) {
//first check the option value - with default memory caching this should help to avoid unnecessary DB operations
if ( get_option( $option ) !== $value ) {
update_option( $option, $value );
}
}
}
/**
* Hack to hide the tax sections for additional tax class rate tables.
*/
public function output_sections_before() {
if ( ! $this->is_enabled() ) {
return;
}
?>
<div style="display: none">
<?php
}
/**
* Hack to hide the tax sections for additional tax class rate tables.
*/
public function output_sections_after() {
if ( ! $this->is_enabled() ) {
return;
}
?></div><?php
}
/**
* TaxJar supports USA, Canada, Australia, and the European Union
* See: https://developers.taxjar.com/api/reference/#countries
*
* @return array Countries supported by TaxJar.
*/
public function get_supported_countries() {
$supported_countries = array_merge(
array(
'US',
'CA',
'AU',
),
WC()->countries->get_european_union_countries()
);
return $supported_countries;
}
/**
* Check if a given country is supported by TaxJar.
*
* @param $country Two character country code.
*
* @return bool Whether or not the country is supported by TaxJar.
*/
public function is_supported_country( $country ) {
return in_array( $country, $this->get_supported_countries() );
}
/**
* Gets the store's location settings.
*
* Modified version of TaxJar's plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L796
*
* @return array
*/
public function get_store_settings() {
$store_settings = array(
'taxjar_zip_code_setting' => WC()->countries->get_base_postcode(),
'store_state_setting' => WC()->countries->get_base_state(),
'store_country_setting' => WC()->countries->get_base_country(),
'taxjar_city_setting' => WC()->countries->get_base_city(),
);
return $store_settings;
}
/**
* @param $message
*/
public function _log( $message ) {
$formatted_message = is_scalar( $message ) ? $message : json_encode( $message );
$this->logger->log( $formatted_message, 'WCS Tax' );
}
/**
* @param $message
*/
public function _error( $message ) {
$formatted_message = is_scalar( $message ) ? $message : json_encode( $message );
$this->logger->error( $formatted_message, 'WCS Tax' );
}
/**
* Wrapper to avoid calling calculate_totals() for admin carts.
*
* @param $wc_cart_object
*/
public function maybe_calculate_totals( $wc_cart_object ) {
if ( ! WC_Connect_Functions::should_send_cart_api_request() ) {
return;
}
$this->calculate_totals( $wc_cart_object );
}
/**
* Calculate tax / totals using TaxJar at checkout
*
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/9d8e725/includes/class-wc-taxjar-integration.php#L475
*
* @return void
*/
public function calculate_totals( $wc_cart_object ) {
global $woocommerce;
// Skip calculations for WC Subscription recurring totals, tax rate already available
if ( class_exists( 'WC_Subscriptions_Cart' ) ) {
if ( 'recurring_total' == WC_Subscriptions_Cart::get_calculation_type() ) {
return;
}
}
// Get all of the required customer params
$taxable_address = $woocommerce->customer->get_taxable_address(); // returns unassociated array
$taxable_address = is_array( $taxable_address ) ? $taxable_address : array();
$to_country = isset( $taxable_address[0] ) && ! empty( $taxable_address[0] ) ? $taxable_address[0] : false;
$to_state = isset( $taxable_address[1] ) && ! empty( $taxable_address[1] ) ? $taxable_address[1] : false;
$to_zip = isset( $taxable_address[2] ) && ! empty( $taxable_address[2] ) ? $taxable_address[2] : false;
$to_city = isset( $taxable_address[3] ) && ! empty( $taxable_address[3] ) ? $taxable_address[3] : false;
$line_items = array();
$cart_taxes = array();
foreach ( $wc_cart_object->coupons as $coupon ) {
if ( method_exists( $coupon, 'get_id' ) ) { // Woo 3.0+
$limit_usage_qty = get_post_meta( $coupon->get_id(), 'limit_usage_to_x_items', true );
if ( $limit_usage_qty ) {
$coupon->set_limit_usage_to_x_items( $limit_usage_qty );
}
}
}
foreach ( $wc_cart_object->get_cart() as $cart_item_key => $cart_item ) {
$product = $cart_item['data'];
$id = $product->get_id();
$quantity = $cart_item['quantity'];
$unit_price = wc_format_decimal( $product->get_price() );
$line_subtotal = wc_format_decimal( $cart_item['line_subtotal'] );
$discount = wc_format_decimal( $cart_item['line_subtotal'] - $cart_item['line_total'] );
$tax_class = explode( '-', $product->get_tax_class() );
$tax_code = '';
if ( ! $product->is_taxable() ) {
$tax_code = '99999';
}
if ( isset( $tax_class ) && is_numeric( end( $tax_class ) ) ) {
$tax_code = end( $tax_class );
}
// Get WC Subscription sign-up fees for calculations
if ( class_exists( 'WC_Subscriptions_Cart' ) ) {
if ( 'none' == WC_Subscriptions_Cart::get_calculation_type() ) {
if ( class_exists( 'WC_Subscriptions_Synchroniser' ) ) {
WC_Subscriptions_Synchroniser::maybe_set_free_trial();
}
$unit_price = WC_Subscriptions_Cart::set_subscription_prices_for_calculation( $unit_price, $product );
}
}
if ( $unit_price && $line_subtotal ) {
array_push($line_items, array(
'id' => $id . '-' . $cart_item_key,
'quantity' => $quantity,
'product_tax_code' => $tax_code,
'unit_price' => $unit_price,
'discount' => $discount,
));
}
}
$this->calculate_tax( array(
'to_city' => $to_city,
'to_state' => $to_state,
'to_country' => $to_country,
'to_zip' => $to_zip,
'shipping_amount' => $woocommerce->shipping->shipping_total,
'line_items' => $line_items,
) );
if ( class_exists( 'WC_Cart_Totals' ) ) { // Woo 3.2+
do_action( 'woocommerce_cart_reset', $wc_cart_object, false );
do_action( 'woocommerce_before_calculate_totals', $wc_cart_object );
new WC_Cart_Totals( $wc_cart_object );
}
foreach ( $this->line_items as $line_item_key => $line_item ) {
if ( isset( $cart_taxes[ $this->rate_ids[ $line_item_key ] ] ) ) {
$cart_taxes[ $this->rate_ids[ $line_item_key ] ] += $line_item->tax_collectable;
} else {
$cart_taxes[ $this->rate_ids[ $line_item_key ] ] = $line_item->tax_collectable;
}
}
// Store the rate ID and the amount on the cart's totals
$wc_cart_object->tax_total = $this->item_collectable;
$wc_cart_object->shipping_tax_total = $this->shipping_collectable;
$wc_cart_object->taxes = $cart_taxes;
if ( isset( $this->rate_ids['shipping'] ) ) {
$wc_cart_object->shipping_taxes = array(
$this->rate_ids['shipping'] => $this->shipping_collectable,
);
}
foreach ( $wc_cart_object->get_cart() as $cart_item_key => $cart_item ) {
$product = $cart_item['data'];
$line_item_key = $product->get_id() . '-' . $cart_item_key;
if ( isset( $this->line_items[ $line_item_key ] ) ) {
$wc_cart_object->cart_contents[ $cart_item_key ]['line_tax'] = $this->line_items[ $line_item_key ]->tax_collectable;
}
}
}
/**
* Calculate tax / totals using TaxJar for backend orders
*
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/9d8e725/includes/class-wc-taxjar-integration.php#L569
*
* @return void
*/
public function calculate_backend_totals( $order_id ) {
$order = wc_get_order( $order_id );
$to_country = isset( $_POST['country'] ) ? strtoupper( wc_clean( $_POST['country'] ) ) : false;
$to_state = isset( $_POST['state'] ) ? strtoupper( wc_clean( $_POST['state'] ) ) : false;
$to_zip = isset( $_POST['postcode'] ) ? strtoupper( wc_clean( $_POST['postcode'] ) ) : false;
$to_city = isset( $_POST['city'] ) ? strtoupper( wc_clean( $_POST['city'] ) ) : false;
$line_items = array();
if ( method_exists( $order, 'get_shipping_total' ) ) {
$shipping = $order->get_shipping_total(); // Woo 3.0+
} else {
$shipping = $order->get_total_shipping(); // Woo 2.6
}
foreach ( $order->get_items() as $item_key => $item ) {
if ( is_object( $item ) ) { // Woo 3.0+
$id = $item->get_product_id();
$quantity = $item->get_quantity();
$discount = wc_format_decimal( $item->get_subtotal() - $item->get_total() );
$tax_class = explode( '-', $item->get_tax_class() );
} else { // Woo 2.6
$id = $item['product_id'];
$quantity = $item['qty'];
$discount = wc_format_decimal( $item['line_subtotal'] - $item['line_total'] );
$tax_class = explode( '-', $item['tax_class'] );
}
$product = wc_get_product( $id );
$unit_price = $product->get_price();
$tax_code = '';
if ( ! $product->is_taxable() ) {
$tax_code = '99999';
}
if ( isset( $tax_class[1] ) && is_numeric( $tax_class[1] ) ) {
$tax_code = $tax_class[1];
}
if ( $unit_price ) {
array_push($line_items, array(
'id' => $id . '-' . $item_key,
'quantity' => $quantity,
'product_tax_code' => $tax_code,
'unit_price' => $unit_price,
'discount' => $discount,
));
}
}
$this->calculate_tax( array(
'to_city' => $to_city,
'to_state' => $to_state,
'to_country' => $to_country,
'to_zip' => $to_zip,
'shipping_amount' => $shipping,
'line_items' => $line_items,
) );
// Add tax rates manually for Woo 3.0+
// Woo 2.6 adds the rates automatically
foreach ( $order->get_items() as $item_key => $item ) {
if ( is_object( $item ) ) { // Woo 3.0+
$product_id = $item->get_product_id();
}
$line_item_key = $product_id . '-' . $item_key;
if ( isset( $this->rate_ids[ $line_item_key ] ) ) {
$rate_id = $this->rate_ids[ $line_item_key ];
if ( class_exists( 'WC_Order_Item_Tax' ) ) { // Woo 3.0+
$item_tax = new WC_Order_Item_Tax();
$item_tax->set_rate( $rate_id );
$item_tax->set_order_id( $order_id );
$item_tax->save();
}
}
}
}
/**
* Set customer zip code and state to store if local shipping option set
*
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c587/includes/class-wc-taxjar-integration.php#L653
*
* @return array
*/
public function append_base_address_to_customer_taxable_address( $address ) {
$store_settings = $this->get_store_settings();
$tax_based_on = '';
list( $country, $state, $postcode, $city ) = $address;
// See WC_Customer get_taxable_address()
// wc_get_chosen_shipping_method_ids() available since Woo 2.6.2+
if ( function_exists( 'wc_get_chosen_shipping_method_ids' ) ) {
if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
$tax_based_on = 'base';
}
} else {
if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && sizeof( array_intersect( WC()->session->get( 'chosen_shipping_methods', array() ), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
$tax_based_on = 'base';
}
}
if ( 'base' == $tax_based_on ) {
$postcode = $store_settings['taxjar_zip_code_setting'];
$city = strtoupper( $store_settings['taxjar_city_setting'] );
}
return array( $country, $state, $postcode, $city );
}
/**
* Calculate sales tax using SmartCalcs
*
* Direct from the TaxJar plugin, without Nexus check.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/9d8e725/includes/class-wc-taxjar-integration.php#L256
*
*
* @return void
*/
public function calculate_tax( $options = array() ) {
global $woocommerce;
$this->_log( ':::: TaxJar Plugin requested ::::' );
// Process $options array and turn them into variables
$options = is_array( $options ) ? $options : array();
extract( array_replace_recursive(array(
'to_country' => null,
'to_state' => null,
'to_zip' => null,
'to_city' => null,
'shipping_amount' => null, // $woocommerce->shipping->shipping_total
'line_items' => null
), $options) );
// Initalize some variables & properties
$store_settings = $this->get_store_settings();
$customer = $woocommerce->customer;
$this->tax_rate = 0;
$this->amount_to_collect = 0;
$this->item_collectable = 0;
$this->shipping_collectable = 0;
$this->freight_taxable = 1;
$this->line_items = array();
$this->has_nexus = 0;
$this->rate_ids = array();
// Strict conditions to be met before API call can be conducted
if ( empty( $to_country ) || empty( $to_zip ) || $customer->is_vat_exempt() ) {
return false;
}
// Setup Vars for API call
$to_zip = explode( ',' , $to_zip );
$to_zip = array_shift( $to_zip );
$from_country = $store_settings['store_country_setting'];
$from_state = $store_settings['store_state_setting'];
$from_zip = $store_settings['taxjar_zip_code_setting'];
$from_city = $store_settings['taxjar_city_setting'];
$shipping_amount = is_null( $shipping_amount ) ? 0.0 : $shipping_amount;
$this->_log( ':::: TaxJar API called ::::' );
$body = array(
'from_country' => $from_country,
'from_state' => $from_state,
'from_city' => $from_city,
'from_zip' => $from_zip,
'to_country' => $to_country,
'to_state' => $to_state,
'to_city' => $to_city,
'to_zip' => $to_zip,
'shipping' => $shipping_amount,
'line_items' => $line_items,
'plugin' => 'woo',
);
$response = $this->smartcalcs_cache_request( wp_json_encode( $body ) );
if ( isset( $response ) ) {
// Log the response
$this->_log( 'Received: ' . $response['body'] );
// Decode Response
$taxjar_response = json_decode( $response['body'] );
$taxjar_response = $taxjar_response->tax;
// Update Properties based on Response
$this->has_nexus = (int) $taxjar_response->has_nexus;
$this->amount_to_collect = $taxjar_response->amount_to_collect;
$this->tax_rate = $taxjar_response->rate;
$this->freight_taxable = (int) $taxjar_response->freight_taxable;
if ( ! empty( $taxjar_response->breakdown ) ) {
if ( ! empty( $taxjar_response->breakdown->shipping ) ) {
$this->shipping_collectable = $taxjar_response->breakdown->shipping->tax_collectable;
}
if ( ! empty( $taxjar_response->breakdown->line_items ) ) {
$line_items = array();
foreach ( $taxjar_response->breakdown->line_items as $line_item ) {
$line_items[ $line_item->id ] = $line_item;
}
$this->line_items = $line_items;
}
}
$this->item_collectable = $this->amount_to_collect - $this->shipping_collectable;
}
// Remove taxes if they are set somehow and customer is exempt
if ( $customer->is_vat_exempt() ) {
$wc_cart_object->remove_taxes();
} elseif ( $this->has_nexus ) {
// Use Woo core to find matching rates for taxable address
$location = array(
'to_country' => $to_country,
'to_state' => $to_state,
'to_zip' => $to_zip,
'to_city' => $to_city,
);
// Add line item tax rates
foreach ( $this->line_items as $line_item_key => $line_item ) {
$line_item_key_split = explode( '-', $line_item_key );
$product_id = $line_item_key_split[0];
$product = wc_get_product( $product_id );
$tax_class = $product->get_tax_class();
$this->create_or_update_tax_rate( $line_item_key, $location, $line_item->combined_tax_rate * 100, $tax_class );
}
// Add shipping tax rate
$this->create_or_update_tax_rate( 'shipping', $location, $this->tax_rate * 100 );
} // End if().
} // End calculate_tax().
/**
* Add or update a native WooCommerce tax rate
*
* Unchanged from the TaxJar plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/9d8e725/includes/class-wc-taxjar-integration.php#L396
*
* @return void
*/
public function create_or_update_tax_rate( $line_item_key, $location, $rate, $tax_class = '' ) {
$tax_rate = array(
'tax_rate_country' => $location['to_country'],
'tax_rate_state' => $location['to_state'],
'tax_rate_name' => sprintf( "%s Tax", $location['to_state'] ),
'tax_rate_priority' => 1,
'tax_rate_compound' => false,
'tax_rate_shipping' => $this->freight_taxable,
'tax_rate' => $rate,
'tax_rate_class' => $tax_class,
);
$wc_rate = WC_Tax::find_rates( array(
'country' => $location['to_country'],
'state' => $location['to_state'],
'postcode' => $location['to_zip'],
'city' => $location['to_city'],
'tax_class' => $tax_class,
) );
if ( ! empty( $wc_rate ) ) {
$this->_log( ':: Tax Rate Found ::' );
$this->_log( $wc_rate );
// Get the existing ID
$rate_id = key( $wc_rate );
// Update Tax Rates with TaxJar rates ( rates might be coming from a cached taxjar rate )
$this->_log( ':: Updating Tax Rate To ::' );
$this->_log( $tax_rate );
WC_Tax::_update_tax_rate( $rate_id, $tax_rate );
} else {
// Insert a rate if we did not find one
$this->_log( ':: Adding New Tax Rate ::' );
$rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
WC_Tax::_update_tax_rate_postcodes( $rate_id, wc_clean( $location['to_zip'] ) );
WC_Tax::_update_tax_rate_cities( $rate_id, wc_clean( $location['to_city'] ) );
}
$this->_log( 'Tax Rate ID Set for ' . $line_item_key . ': ' . $rate_id );
$this->rate_ids[ $line_item_key ] = $rate_id;
}
/**
* Wrap SmartCalcs API requests in a transient-based caching layer.
*
* Modified from TaxJar's plugin (removed use of TLC Transients)
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L463
*
* @param $json
*
* @return mixed|WP_Error
*/
public function smartcalcs_cache_request( $json ) {
$cache_key = 'wcs_tax_' . hash( 'md5', $json );
$response = get_transient( $cache_key );
if ( false === $response ) {
$response = $this->smartcalcs_request( $json );
if ( 200 == wp_remote_retrieve_response_code( $response ) ) {
set_transient( $cache_key, $response, HOUR_IN_SECONDS );
}
}
return $response;
}
/**
* Make a TaxJar SmartCalcs API request through the WCS proxy.
*
* Modified from TaxJar's plugin.
* See: https://github.com/taxjar/taxjar-woocommerce-plugin/blob/82bf7c58/includes/class-wc-taxjar-integration.php#L440
*
* @param $json
*
* @return array|WP_Error
*/
public function smartcalcs_request( $json ) {
$path = trailingslashit( self::PROXY_PATH ) . 'taxes';
$this->_log( 'Requesting: ' . $path . ' - ' . $json );
$response = $this->api_client->proxy_request( $path, array(
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => $json,
) );
if ( is_wp_error( $response ) ) {
$this->_error( 'Error retrieving the tax rates. Received (' . $response->get_error_code() . '): ' . $response->get_error_message() );
} elseif ( 200 == $response['response']['code'] ) {
return $response;
} else {
$this->_error( 'Error retrieving the tax rates. Received (' . $response['response']['code'] . '): ' . $response['body'] );
}
}
/**
* 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
*/
public 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;
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_contents();
ob_end_clean();
$upload_dir = wp_upload_dir();
file_put_contents( $upload_dir['basedir'] . '/taxjar-wc_tax_rates-' . date( 'm-d-Y' ) . '-' . time() . '.csv', $csv );
// Delete all tax rates
$wpdb->query( 'TRUNCATE ' . $wpdb->prefix . 'woocommerce_tax_rates' );
$wpdb->query( 'TRUNCATE ' . $wpdb->prefix . 'woocommerce_tax_rate_locations' );
}
}

View File

@@ -0,0 +1,126 @@
<?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;
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() ) {
if ( ! function_exists( 'jetpack_tracks_record_event' ) ) {
$this->debug( 'Error. jetpack_tracks_record_event is not defined.' );
return;
}
$user = wp_get_current_user();
$site_url = get_option( 'siteurl' );
$wcs_version = WC_Connect_Loader::get_wcs_version();
// Check for WooCommerce
$wc_version = 'unavailable';
if ( function_exists( 'WC' ) ) {
$wc_version = WC()->version;
}
// Check for Jetpack
$jp_version = 'unavailable';
if ( defined( 'JETPACK__VERSION' ) ) {
$jp_version = JETPACK__VERSION;
}
$is_atomic = WC_Connect_Jetpack::is_atomic_site();
$jetpack_blog_id = -1;
if ( class_exists( 'Jetpack_Options' ) && method_exists( 'Jetpack_Options', 'get_option' ) ) {
$jetpack_blog_id = Jetpack_Options::get_option( 'id' );
}
if ( ! is_array( $data ) ) {
$data = array();
}
$data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
$data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
$data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
$data['blog_url'] = $site_url;
$data['blog_id'] = $jetpack_blog_id;
$data['wcs_version'] = $wcs_version;
$data['jetpack_version'] = $jp_version;
$data['is_atomic'] = $is_atomic;
$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 );
jetpack_tracks_record_event( $user, $event_type, $data );
}
protected function debug( $message ) {
if ( ! is_null( $this->logger ) ) {
$this->logger->log( $message );
}
}
}
}

View File

@@ -0,0 +1,93 @@
<?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;
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;
}
public function get() {
// Always get a fresh list of payment methods when hitting this endpoint
$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' );
}
$master_user = WC_Connect_Jetpack::get_master_user();
if ( is_a( $master_user, 'WP_User' ) ) {
$connected_data = WC_Connect_Jetpack::get_connected_user_data( $master_user->ID );
$email = $connected_data['email'];
} else {
$email = '';
}
return new WP_REST_Response( array(
'success' => true,
'storeOptions' => $this->settings_store->get_store_options(),
'formData' => $this->settings_store->get_account_settings(),
'formMeta' => array(
'can_manage_payments' => $this->can_user_manage_payment_methods(),
'can_edit_settings' => true,
'master_user_name' => is_a( $master_user, 'WP_User' ) ? $master_user->display_name : '',
'master_user_login' => is_a( $master_user, 'WP_User' ) ? $master_user->user_login : '',
'master_user_email' => $email,
'payment_methods' => $this->payment_methods_store->get_payment_methods(),
'warnings' => array( 'payment_methods' => $payment_methods_warning ),
),
), 200 );
}
public function post( $request ) {
$settings = $request->get_json_params();
if ( ! $this->can_user_manage_payment_methods() ) {
// Ignore the user-provided payment method ID if he doesn'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 );
}
private function can_user_manage_payment_methods() {
global $current_user;
$master_user = WC_Connect_Jetpack::get_master_user();
return WC_Connect_Jetpack::is_development_mode() ||
( is_a( $master_user, 'WP_User' ) && $current_user->ID === $master_user->ID );
}
}

View File

@@ -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' ];
$name = $address[ 'name' ];
$company = $address[ 'company' ];
$phone = $address[ 'phone' ];
unset( $address[ 'name' ], $address[ 'company' ], $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,
);
}
$response->normalized->name = $name;
$response->normalized->company = $company;
$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 current_user_can( 'manage_woocommerce' ); // Only an admin can normalize the origin address
}
return true; // non-authenticated service for the 'destination' address
}
}

View File

@@ -0,0 +1,110 @@
<?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' ),
),
) );
}
}
/**
* 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 );
}
/**
* Validate the requester's permissions
*/
public function check_permission( $request ) {
return current_user_can( 'manage_woocommerce' );
}
}

View File

@@ -0,0 +1,48 @@
<?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_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 $service_schemas_store ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->service_schemas_store = $service_schemas_store;
}
public function get() {
return new WP_REST_Response( array(
'success' => true,
'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()
)
), 200 );
}
public function post( $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 );
}
}

View File

@@ -0,0 +1,45 @@
<?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 );
}
}

View File

@@ -0,0 +1,97 @@
<?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_merge(
array( 'status' => 400 ),
$validation_result->get_error_data()
)
);
$this->logger->log( $error, __CLASS__ );
return $error;
}
return new WP_REST_Response( array( 'success' => true ), 200 );
}
}

View File

@@ -0,0 +1,120 @@
<?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;
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 ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->shipping_label = $shipping_label;
}
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[ 'payment_method_id' ] = $this->settings_store->get_selected_payment_method_id();
$settings[ 'order_id' ] = $order_id;
$service_names = array();
foreach ( $settings[ 'packages' ] as $index => $package ) {
$service_names[] = $package[ 'service_name' ];
unset( $package[ 'service_name' ] );
$settings[ 'packages' ][ $index ] = $package;
}
$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,
);
$package = $settings[ 'packages' ][ $index ];
$box_id = $package[ 'box_id' ];
if ( 'individual' === $box_id ) {
$label_meta[ 'package_name' ] = __( 'Individual packaging', 'woocommerce-services' );
} else if ( isset( $package_lookup[ $box_id ] ) ) {
$label_meta[ 'package_name' ] = $package_lookup[ $box_id ][ 'name' ];
} else {
$label_meta[ 'package_name' ] = __( 'Unknown package', 'woocommerce-services' );
}
$product_names = array();
foreach ( $package[ 'products' ] as $product_id ) {
$product = wc_get_product( $product_id );
if ( $product ) {
$product_names[] = $product->get_title();
} else {
$order = wc_get_order( $order_id );
$product_names[] = WC_Connect_Compatibility::instance()->get_product_name_from_order( $product_id, $order );
}
}
$label_meta[ 'product_names' ] = $product_names;
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,
);
}
}

View File

@@ -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' ];
die();
}
}

View File

@@ -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' ];
die();
}
}
}

View File

@@ -0,0 +1,45 @@
<?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,
);
}
}

View File

@@ -0,0 +1,35 @@
<?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_id>\d+)';
public function get( $request ) {
$response = $this->api_client->get_label_status( $request[ '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 );
return array(
'success' => true,
'label' => $label,
);
}
}

View File

@@ -0,0 +1,45 @@
<?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';
/**
*
* @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();
$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' ] );
$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 array(
'success' => true,
'rates' => property_exists( $response, 'rates' ) ? $response->rates : new stdClass(),
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Stripe_Account_Controller' ) ) {
return;
}
class WC_REST_Connect_Stripe_Account_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/stripe/account';
private $stripe;
public function __construct( WC_Connect_Stripe $stripe, WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->stripe = $stripe;
}
public function get( $request ) {
$response = $this->stripe->get_account_details();
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 400
)
);
}
return array(
'success' => true,
'account_id' => $response->accountId,
'display_name' => $response->displayName,
'email' => $response->email,
'business_logo' => $response->businessLogo,
'legal_entity' => array(
'first_name' => $response->legalEntity->firstName,
'last_name' => $response->legalEntity->lastName
),
'payouts_enabled' => $response->payoutsEnabled
);
}
public function post( $request ) {
$data = $request->get_json_params();
$response = $this->stripe->create_account( $data['email'], $data['country'] );
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 400
)
);
}
return array(
'success' => true,
'account_id' => $response->accountId,
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Stripe_Deauthorize_Controller' ) ) {
return;
}
class WC_REST_Connect_Stripe_Deauthorize_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/stripe/account/deauthorize';
private $stripe;
public function __construct( WC_Connect_Stripe $stripe, WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->stripe = $stripe;
}
public function post( $request ) {
$response = $this->stripe->deauthorize_account();
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 400
)
);
}
return array(
'success' => true,
'account_id' => $response->accountId,
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Stripe_Oauth_Connect_Controller' ) ) {
return;
}
class WC_REST_Connect_Stripe_Oauth_Connect_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/stripe/oauth/connect';
private $stripe;
public function __construct( WC_Connect_Stripe $stripe, WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->stripe = $stripe;
}
public function post( $request ) {
$data = $request->get_json_params();
$response = $this->stripe->connect_oauth( $data['state'], $data['code'] );
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 400
)
);
}
return array(
'success' => true,
'account_id' => $response->accountId,
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WC_REST_Connect_Stripe_Oauth_Init_Controller' ) ) {
return;
}
class WC_REST_Connect_Stripe_Oauth_Init_Controller extends WC_REST_Connect_Base_Controller {
protected $rest_base = 'connect/stripe/oauth/init';
private $stripe;
public function __construct( WC_Connect_Stripe $stripe, WC_Connect_API_Client $api_client, WC_Connect_Service_Settings_Store $settings_store, WC_Connect_Logger $logger ) {
parent::__construct( $api_client, $settings_store, $logger );
$this->stripe = $stripe;
}
public function post( $request ) {
$data = $request->get_json_params();
$response = $this->stripe->get_oauth_url( $data['returnUrl'] );
if ( is_wp_error( $response ) ) {
$this->logger->log( $response, __CLASS__ );
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 400
)
);
}
return array(
'success' => true,
'oauthUrl' => $response,
);
}
}

View File

@@ -0,0 +1,45 @@
<?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' );
}
}