'woocommerce-square',
'gateways' => array(
self::GATEWAY_ID => Gateway::class,
self::CASH_APP_PAY_GATEWAY_ID => Cash_App_Pay_Gateway::class,
self::GIFT_CARD_PAY_GATEWAY_ID => Gift_Card::class,
),
'require_ssl' => true,
'supports' => array(
self::FEATURE_CAPTURE_CHARGE,
self::FEATURE_CUSTOMER_ID,
self::FEATURE_MY_PAYMENT_METHODS,
),
'dependencies' => array(
'php_extensions' => array( 'curl', 'json', 'mbstring' ),
),
)
);
$this->includes();
/**
* Fires upon plugin loaded (legacy hook).
*
* @since 1.0.0
*/
do_action( 'wc_square_loaded' );
add_action( 'woocommerce_register_taxonomy', array( $this, 'init_taxonomies' ) );
add_action( 'admin_notices', array( $this, 'add_admin_notices' ) );
add_filter( 'woocommerce_locate_template', array( $this, 'locate_template' ), 20, 3 );
add_filter( 'woocommerce_locate_core_template', array( $this, 'locate_template' ), 20, 3 );
add_action( 'action_scheduler_init', array( $this, 'schedule_token_migration_job' ) );
add_action( 'wc_square_init_payment_token_migration_v2', array( $this, 'register_payment_tokens_migration_scheduler' ) );
add_action( 'wc_square_init_payment_token_migration', '__return_false' );
}
/**
* Includes required classes.
*
* @since 2.0.0
*/
private function includes() {
$this->connection_handler = new Handlers\Connection( $this );
$this->sync_handler = new Sync( $this );
// background export must be loaded all the time, because otherwise background jobs simply won't work
require_once $this->get_framework_path() . '/Utilities/WP_Background_Job_Handler.php';
$this->background_job_handler = new Background_Job();
$this->ajax_handler = new AJAX();
$this->email_handler = new Email();
$this->order_handler = new Order();
if ( class_exists( '\WooCommerce\Square\Handlers\Async_Request' ) ) {
$this->async_request_handler = new Async_Request();
}
}
/**
* Adds API request logging.
*
* @internal
*
* @since 2.0.0
*/
public function add_api_request_logging() {
if ( ! has_action( 'wc_square_api_request_performed' ) ) {
add_action( 'wc_square_api_request_performed', array( $this, 'log_api_request' ), 10, 2 );
}
}
/**
* Logs an API request & response.
*
* @since 2.0.0
*
* @param array $request request data
* @param array $response response data
* @param string|null $log_id log ID
*/
public function log_api_request( $request, $response, $log_id = null ) {
if ( $this->get_settings_handler() && $this->get_settings_handler()->is_debug_enabled() ) {
parent::log_api_request( $request, $response, $log_id );
}
}
/**
* If debug logging is enabled, saves errors or messages to Square Log
*
* @since 2.2.4
* @param string $message error or message to save to log
* @param string $log_id optional log id to segment the files by, defaults to plugin id
*/
public function log( $message, $log_id = null ) {
if ( $this->get_settings_handler() && $this->get_settings_handler()->is_debug_enabled() ) {
parent::log( $message, $log_id );
}
}
/**
* Initializes the lifecycle handler.
*
* @since 2.0.0
*/
public function init_lifecycle_handler() {
$this->lifecycle_handler = new Lifecycle( $this );
}
/**
* Registers custom taxonomies.
*
* @internal
*
* @since 2.0.0
*/
public function init_taxonomies() {
Product::init_taxonomies();
}
/**
* Initializes the general plugin functionality.
*
* @since 2.0.0
*/
public function init_plugin() {
$this->settings_handler = new Settings( $this );
$this->products_handler = new Products( $this );
if ( ! $this->admin_handler && is_admin() ) {
$this->admin_handler = new Admin( $this );
}
// WooPayments compatibility.
$wcpay_compatibility = new WC_Payments_Compatibility();
$wcpay_compatibility->init();
/**
* @see wc_square_initialized
* @since 2.0.0
*/
do_action( 'wc_square_initialized' );
}
/**
* Locates the WooCommerce template files from our templates directory.
*
* @internal
*
* @since 2.0.0
*
* @param string $template already found template
* @param string $template_name searchable template name
* @param string $template_path template path
* @return string search result for the template
*/
public function locate_template( $template, $template_name, $template_path ) {
// only keep looking if no custom theme template was found
// or if a default WooCommerce template was found
if ( ! $template || Square_Helper::str_starts_with( $template, WC()->plugin_path() ) ) {
// set the path to our templates directory
$plugin_path = $this->get_plugin_path() . '/templates/';
// if a template is found, make it so
if ( is_readable( $plugin_path . $template_name ) ) {
$template = $plugin_path . $template_name;
}
}
return $template;
}
/** Admin methods *************************************************************************************************/
/**
* Adds admin notices.
*
* @since 2.0.0
*/
public function add_admin_notices() {
// show any one-off messages
$this->get_message_handler()->show_messages();
// display a notice if the auto-refresh failed
if ( get_option( 'wc_square_refresh_failed', false ) ) {
$message = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag */
__( 'Heads up! There may be a problem with your connection to Square. In order to continue accepting payments, please %1$sdisconnect and re-connect your site%2$s.', 'woocommerce-square' ),
'',
''
);
$this->get_admin_notice_handler()->add_admin_notice(
$message,
'refresh-failed',
array(
'dismissible' => false,
'notice_class' => 'notice-warning',
)
);
}
if ( $this->get_settings_handler()->is_connected() ) {
$message = '' . __( 'You are connected to Square!', 'woocommerce-square' ) . '';
// prompt to set a location if not set
if ( ! $this->get_settings_handler()->get_location_id() ) {
if ( $this->is_plugin_settings() ) {
$instruction = __( 'To get started, set your business location.', 'woocommerce-square' );
} else {
$instruction = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag */
__( 'Visit the %1$splugin settings%2$s to set your business location.', 'woocommerce-square' ),
'',
''
);
}
$this->get_admin_notice_handler()->add_admin_notice( $message . ' ' . $instruction, 'set-location' );
} elseif ( ! $this->get_sync_handler()->get_last_synced_at() && $this->get_settings_handler()->is_product_sync_enabled() ) {
$message = __( 'You are ready to sync products!', 'woocommerce-square' );
if ( ! empty( Product::get_products_synced_with_square() ) ) {
$instruction = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - product count, %3$s - tag, %4$s - tag, %5$s - tag */
__( '%1$s%2$d products%3$s are marked "sync with Square". %4$sStart a new sync now »%5$s', 'woocommerce-square' ),
'',
count( Product::get_products_synced_with_square() ),
'',
'',
''
);
} else {
$instruction = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - tag, %4$s - tag */
__( '%1$sNo products%2$s are marked "sync with Square". %3$sUpdate your products to sync data »%4$s', 'woocommerce-square' ),
'',
'',
'',
''
);
}
$this->get_admin_notice_handler()->add_admin_notice( $message . ' ' . $instruction, 'set-location' );
}
// a notice for when WC stock handling is globally disabled
if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) && $this->get_settings_handler()->is_inventory_sync_enabled() ) {
$message = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag */
__( 'Heads up! Square is configured to sync product inventory, but WooCommerce stock management is disabled. Please %1$senable stock management%2$s to ensure product inventory counts are kept in sync.', 'woocommerce-square' ),
'',
''
);
$this->get_admin_notice_handler()->add_admin_notice(
$message,
'enable-wc-sync',
array(
'notice_class' => 'notice-warning',
)
);
}
} else {
$instruction = '';
if ( wc_square()->get_dependency_handler()->meets_php_dependencies() ) {
if ( $this->is_plugin_settings() ) {
$instruction = __( 'To get started, connect with Square.', 'woocommerce-square' );
} else {
$instruction = sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag */
__( 'To get started, %1$sconnect with Square »%2$s', 'woocommerce-square' ),
'',
''
);
}
}
$message = sprintf(
/* translators: Placeholders: %1$s - plugin name */
__( 'Thanks for installing %1$s!', 'woocommerce-square' ),
esc_html( $this->get_plugin_name() )
);
$this->get_admin_notice_handler()->add_admin_notice( $message . ' ' . $instruction, 'connect' );
}
// add a notice for out-of-bounds base locations
$this->add_base_location_admin_notice();
// add a notice when no refresh token is available
$this->add_missing_refresh_token_notice();
// add a tax-inclusive warning to product pages
$this->add_tax_inclusive_pricing_notice();
if ( get_option( 'wc_square_updated_to_2_0_0' ) ) {
$this->get_admin_notice_handler()->add_admin_notice(
sprintf(
/* translators: Placeholders: %1$s - plugin name, %2$ - plugin version number, %3$s - opening HTML link tag, %4$s - closing HTML link tag, %5$s - opening HTML link tag, %6$s - closing HTML link tag*/
esc_html__( '%1$s has been updated to version %2$s. In order to continue syncing product inventory, please make sure to disconnect and reconnect with Square from the %3$splugin settings%4$s and re-sync your products. Read more in the %5$supdated documentation%6$s.', 'woocommerce-square' ),
'' . esc_html( $this->get_plugin_name() ) . '',
$this->get_version(),
'',
'',
'',
''
),
'updated-to-v2',
array( 'notice_class' => 'notice-warning' )
);
}
}
/**
* Adds a notice for out-of-bounds base locations.
*
* @since 2.0.0
*/
protected function add_base_location_admin_notice() {
$accepted_countries = array(
'US',
'CA',
'GB',
'AU',
'JP',
'IE',
'FR',
'ES',
);
$base_location = wc_get_base_location();
if ( isset( $base_location['country'] ) && ! in_array( $base_location['country'], $accepted_countries, true ) ) {
$this->get_admin_notice_handler()->add_admin_notice(
sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - 2-character country code, %4$s - comma separated list of 2-character country codes */
__( '%1$sWooCommerce Square:%2$s Your base country is %3$s, but Square can’t accept transactions from merchants outside of %4$s.', 'woocommerce-square' ),
'',
'',
esc_html( $base_location['country'] ),
esc_html( Square_Helper::list_array_items( $accepted_countries ) )
),
'wc-square-base-location',
array(
'notice_class' => 'notice-error',
)
);
}
}
/**
* Adds a notice if no refresh token has been cached.
*
* @since 2.0.5
*/
protected function add_missing_refresh_token_notice() {
if ( $this->get_settings_handler()->is_sandbox() ) {
return;
}
$refresh_token = '';
$settings_handler = $this->get_settings_handler();
if ( method_exists( $settings_handler, 'get_access_token' ) ) {
$access_token = $settings_handler->get_access_token();
if ( empty( $access_token ) ) {
// We are already in a disconnected state, don't show the warning.
return;
}
}
if ( method_exists( $settings_handler, 'get_refresh_token' ) ) {
$refresh_token = $settings_handler->get_refresh_token();
}
if ( empty( $refresh_token ) ) {
$this->get_admin_notice_handler()->add_admin_notice(
sprintf(
/* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - tag, %4$s - tag */
__( '%1$sWooCommerce Square:%2$s Automatic refreshing of the connection to Square is inactive. Please disconnect and reconnect to resolve.', 'woocommerce-square' ),
'',
''
),
'wc-square-missing-refresh-token',
array(
'dismissible' => false,
'notice_class' => 'notice-error',
)
);
}
}
/**
* Adds a tax-inclusive admin warning to product pages.
*
* @since 2.0.0
*/
protected function add_tax_inclusive_pricing_notice() {
global $typenow;
// only show on product edit pages when configured that prices include tax
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce not required, not writing any changes.
if ( 'product' === $typenow && isset( $_GET['action'], $_GET['post'] ) && 'edit' === $_GET['action'] && wc_prices_include_tax() && $this->get_settings_handler()->is_product_sync_enabled() ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce not required, not writing any changes.
$product = wc_get_product( (int) $_GET['post'] );
// only show for products configured as taxable and sync with Square
if ( $product instanceof \WC_Product && $product->is_taxable() && Product::is_synced_with_square( $product ) ) {
$this->get_admin_notice_handler()->add_admin_notice(
sprintf(
/* translators: Placeholders: %1$s = tag, %2$s = tag */
__( '%1$sWooCommerce Square:%2$s Product prices are entered inclusive of tax, but Square does not support syncing tax-inclusive prices. Please make sure your Square tax rates match your WooCommerce tax rates.', 'woocommerce-square' ),
'',
''
),
'wc-square-tax-inclusive',
array(
'notice_class' => 'notice-warning',
)
);
}
}
}
/**
* Adds admin notices for currency issues.
*
* @since 2.0.0
*/
protected function add_currency_admin_notices() {
parent::add_currency_admin_notices();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce not required, only showing a notice.
if ( isset( $_GET['page'] ) && 'wc-settings' === $_GET['page'] && $this->get_settings_handler()->is_connected() ) {
foreach ( $this->get_settings_handler()->get_locations() as $location ) {
if ( $this->get_settings_handler()->get_location_id() === $location->getId() && get_woocommerce_currency() !== $location->getCurrency() ) {
$this->get_admin_notice_handler()->add_admin_notice(
sprintf(
/* translators: Placeholders: %1$s = store currency, %2$s = configured Square business location currency, %3$s = tag, %4$s = tag, %5$s = tag, %6$s = tag */
__( 'Heads up! Your store currency is %1$s but your configured Square business location currency is %2$s, so payments cannot be processed. Please %3$schoose a different business location%4$s or change your %5$sshop currency%6$s.', 'woocommerce-square' ),
'' . esc_html( get_woocommerce_currency() ) . '',
'' . esc_html( $location->getCurrency() ) . '',
'',
'',
'',
''
),
'wc-square-currency-mismatch',
array(
'notice_class' => 'notice-error',
)
);
}
}
}
}
/** Helper methods ************************************************************************************************/
/**
* Returns an idempotency key to be used in Square API requests.
*
* @since 2.0.0
*
* @param string $key_input
* @param bool $append_key_input
* @return string
*/
public function get_idempotency_key( $key_input = '', $append_key_input = true ) {
if ( '' === $key_input ) {
$key_input = uniqid( '', false );
}
/**
* Filters an idempotency key.
*
* @since 2.0.0
*
* @param string $key_input
*/
return apply_filters( 'wc_square_idempotency_key', sha1( get_option( 'siteurl' ) . $key_input ) . ( $append_key_input ? ':' . $key_input : '' ) );
}
/** Conditional methods *******************************************************************************************/
/**
* Determines if viewing the plugin settings.
*
* @since 2.0.0
*
* @return bool
*/
public function is_plugin_settings() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce note required, read-only check.
return parent::is_plugin_settings() || ( isset( $_GET['page'], $_GET['tab'] ) && 'wc-settings' === $_GET['page'] && self::PLUGIN_ID === $_GET['tab'] );
}
/**
* Determines if viewing the gateway settings.
*
* @since 2.3.0
*
* @return bool
*/
public function is_gateway_settings() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce note required, read-only check.
return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'checkout' === $_GET['tab'] && self::GATEWAY_ID === $_GET['section'];
}
/** Getter methods ************************************************************************************************/
/**
* Gets the main Square API handler.
*
* @since 2.0.0
*
* @param string|null $access_token API access token
* @return API
*/
public function get_api( $access_token = null, $is_sandbox = null ) {
if ( ! $access_token ) {
$access_token = $this->get_settings_handler()->get_access_token();
}
if ( is_null( $is_sandbox ) ) {
$is_sandbox = $this->get_settings_handler()->is_sandbox();
}
return new API( $access_token, $is_sandbox );
}
/**
* Gets the connection handler.
*
* @since 2.0.0
*
* @return Handlers\Connection
*/
public function get_connection_handler() {
return $this->connection_handler;
}
/**
* Gets the sync handler instance.
*
* @since 2.0.0
*
* @return Sync
*/
public function get_sync_handler() {
return $this->sync_handler;
}
/**
* Gets the background sync handler instance.
*
* @since 2.0.0
*
* @return Background_Job
*/
public function get_background_job_handler() {
return $this->background_job_handler;
}
/**
* Gets the settings handler instance.
*
* @since 2.0.0
*
* @return Settings
*/
public function get_settings_handler() {
return $this->settings_handler;
}
/**
* Gets the admin handler instance.
*
* @since 2.0.0
*
* @return Admin|null
*/
public function get_admin_handler() {
// throw a notice if calling before admin_init
Square_Helper::maybe_doing_it_early( 'admin_init', __METHOD__, '2.0.0' );
return $this->admin_handler;
}
/**
* Gets the email handler instance.
*
* @since 2.0.0
*
* @return Email
*/
public function get_email_handler() {
return $this->email_handler;
}
/**
* Gets the order handler instance.
*
* @since 2.0.0
*
* @return Order
*/
public function get_order_handler() {
return $this->order_handler;
}
/**
* Get the products handler instance/
*
* @since 2.0.8
*
* @return Products
*/
public function get_products_handler() {
return $this->products_handler;
}
/**
* Gets the Asynchronous request handler instance.
*
* @since 4.1.0
*
* @return Async_Request
*/
public function get_async_request_handler() {
return $this->async_request_handler;
}
/**
* Gets the plugin name.
*
* @since 2.0.0
*
* @return string
*/
public function get_plugin_name() {
return __( 'WooCommerce Square', 'woocommerce-square' );
}
/**
* Gets the settings URL.
*
* @since 2.0.0
*
* @param null|string $gateway_id gateway ID
* @return string
*/
public function get_settings_url( $gateway_id = null ) {
$params = array(
'page' => 'wc-settings',
'tab' => self::PLUGIN_ID,
);
// All usage of this return value has been escaped late.
// nosemgrep audit.php.wp.security.xss.query-arg
return add_query_arg( $params, admin_url( 'admin.php' ) );
}
/**
* Gets the Setup Wizard URL.
*
* @since 4.7.0
*
* @param string $step Step to go to.
*
* @return string
*/
public function get_square_onboarding_url( $step = '' ) {
$params = array(
'page' => 'woocommerce-square-onboarding',
);
// Add 'step' if $step is not empty.
if ( ! empty( $step ) ) {
$params['step'] = $step;
}
// All usage of this return value has been escaped late.
// nosemgrep audit.php.wp.security.xss.query-arg
return add_query_arg( $params, admin_url( 'admin.php' ) );
}
/**
* Gets the sale page URL.
*
* @since 2.0.0
*
* @return string
*/
public function get_sales_page_url() {
return 'https://woocommerce.com/products/square/';
}
/**
* Gets the documentation URL.
*
* @since 2.0.0
*
* @return string
*/
public function get_documentation_url() {
return 'https://docs.woocommerce.com/document/woocommerce-square/';
}
/**
* Gets the plugin reviews page URL.
*
* Used for the 'Reviews' plugin action and review prompts.
*
* @since 2.1.7
*
* @return string
*/
public function get_reviews_url() {
return $this->get_sales_page_url() ? $this->get_sales_page_url() . '#comments' : '';
}
/**
* Gets the support URL.
*
* @since 2.0.0
*
* @return string
*/
public function get_support_url() {
return 'https://woocommerce.com/my-account/create-a-ticket/?select=1770503';
}
/**
* Gets __DIR__.
*
* @since 2.0.0
*
* @return string
*/
protected function get_file() {
return __DIR__;
}
/**
* Gets the singleton instance of the plugin.
*
* @since 2.0.0
*
* @return Plugin
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Schedules the migration of payment tokens.
*
* @since 3.8.0
*/
public function schedule_token_migration_job() {
if ( false !== get_option( 'wc_square_payment_token_migration_complete' ) ) {
return;
}
// Remove all OLD scheduled actions to cleanup DB.
// TODO: Remove this in next release.
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE hook = 'wc_square_init_payment_token_migration'" );
if ( false === as_has_scheduled_action( 'wc_square_init_payment_token_migration_v2' ) ) {
as_enqueue_async_action( 'wc_square_init_payment_token_migration_v2', array( 'page' => 1 ) );
}
}
/**
* Migrates payment token from user_meta to WC_Payment_Token_CC.
*
* @param integer $page Pagination number.
* @since 3.8.0
*/
public function register_payment_tokens_migration_scheduler( $page ) {
$payment_tokens_handler = wc_square()->get_gateway()->get_payment_tokens_handler();
$meta_key = $payment_tokens_handler->get_user_meta_name();
// Get 5 users in a batch.
$users = get_users(
array(
'fields' => array( 'ID' ),
'number' => 5,
'paged' => $page,
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => $meta_key,
'compare' => 'EXISTS',
),
),
)
);
// If users array is empty, then set status in options to indicate migration is complete.
if ( empty( $users ) ) {
$payment_tokens_handler->clear_all_transients();
update_option( 'wc_square_payment_token_migration_complete', true );
return;
}
// Re-run scheduler for the next page of users.
as_enqueue_async_action( 'wc_square_init_payment_token_migration_v2', array( 'page' => $page + 1 ) );
foreach ( $users as $user ) {
$user_payment_tokens = get_user_meta( $user->id, $meta_key, true );
if ( ! is_array( $user_payment_tokens ) || empty( $user_payment_tokens ) ) {
continue;
}
foreach ( $user_payment_tokens as $token => $user_payment_token_data ) {
// Check if token already exists in WC_Payment_Token_CC.
if ( $payment_tokens_handler->user_has_token( $user->id, $token ) ) {
continue;
}
$payment_token = new Square_Credit_Card_Payment_Token();
$payment_token->set_token( $token );
$payment_token->set_card_type( $user_payment_token_data['card_type'] );
$payment_token->set_last4( $user_payment_token_data['last_four'] );
$payment_token->set_expiry_month( $user_payment_token_data['exp_month'] );
$payment_token->set_expiry_year( $user_payment_token_data['exp_year'] );
$payment_token->set_user_id( $user->id );
$payment_token->set_gateway_id( wc_square()->get_gateway()->get_id() );
if ( isset( $user_payment_token_data['nickname'] ) ) {
$payment_token->set_nickname( $user_payment_token_data['nickname'] );
}
$payment_token->save();
}
}
}
}