get_error_message() ), self::settings_url() ) ); exit; } wp_safe_redirect( esc_url_raw( $result['connect_url'] ) ); exit; } public static function handle_reopen() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to connect QuickSync.', 'quicksync-pro-for-woocommerce' ) ); } check_admin_referer( 'quicksync_reopen' ); $url = get_transient( 'quicksync_connect_url' ); if ( ! $url ) { wp_safe_redirect( add_query_arg( 'quicksync_error', rawurlencode( __( 'That link expired — connect again.', 'quicksync-pro-for-woocommerce' ) ), self::settings_url() ) ); exit; } wp_safe_redirect( esc_url_raw( $url ) ); exit; } public static function ajax_start_connect() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to connect QuickSync.', 'quicksync-pro-for-woocommerce' ) ) ); } check_ajax_referer( 'quicksync_connect', 'nonce' ); $result = self::create_handoff(); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( $result ); } public static function handle_claim() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to connect QuickSync.', 'quicksync-pro-for-woocommerce' ) ); } check_admin_referer( 'quicksync_claim' ); $code = isset( $_POST['quicksync_code'] ) ? sanitize_text_field( wp_unslash( $_POST['quicksync_code'] ) ) : ''; if ( '' === $code ) { wp_safe_redirect( add_query_arg( 'quicksync_error', rawurlencode( __( 'Paste the pairing code from QuickSync.', 'quicksync-pro-for-woocommerce' ) ), self::settings_url() ) ); exit; } $replace_id = get_transient( 'quicksync_handoff_id' ); $replace_secret = get_transient( 'quicksync_handoff_secret' ); $key = self::provision_key(); if ( is_wp_error( $key ) ) { wp_safe_redirect( add_query_arg( 'quicksync_error', rawurlencode( $key->get_error_message() ), self::settings_url() ) ); exit; } $headers = array( 'Content-Type' => 'application/json' ); $body = array( 'code' => $code, 'home_url' => QuickSync_Plugin::current_home_url(), 'consumer_key' => $key['consumer_key'], 'consumer_secret' => $key['consumer_secret'], 'key_id' => (string) $key['key_id'], 'version' => QUICKSYNC_VERSION, ); if ( $replace_id && $replace_secret ) { $headers['X-QuickSync-Handoff-Secret'] = $replace_secret; $body['replace_handoff_id'] = $replace_id; } $response = wp_remote_post( QUICKSYNC_API_BASE . '/store/woocommerce/plugin/claim/', array( 'timeout' => 15, 'headers' => $headers, 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { self::revoke_key( $key['key_id'] ); wp_safe_redirect( add_query_arg( 'quicksync_error', rawurlencode( $response->get_error_message() ), self::settings_url() ) ); exit; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['ok'] ) || empty( $body['token'] ) ) { self::revoke_key( $key['key_id'] ); $message = ! empty( $body['error'] ) ? $body['error'] : __( 'That code is wrong or has expired. Get a new one from QuickSync.', 'quicksync-pro-for-woocommerce' ); if ( 'Invalid or expired pairing code' === $message ) { $message = __( 'That code is wrong or has expired. Get a new one from QuickSync.', 'quicksync-pro-for-woocommerce' ); } wp_safe_redirect( add_query_arg( 'quicksync_error', rawurlencode( $message ), self::settings_url() ) ); exit; } self::store_credentials( $body, $key['key_id'] ); wp_safe_redirect( add_query_arg( 'quicksync_connected', '1', self::settings_url() ) ); exit; } public static function handle_disconnect() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to disconnect QuickSync.', 'quicksync-pro-for-woocommerce' ) ); } check_admin_referer( 'quicksync_disconnect' ); QuickSync_Identity::notify_disconnect(); self::revoke_stored_keys(); QuickSync_Plugin::update_options( array( 'token' => '', 'hmac_secret' => '', 'store_id' => '', 'key_id' => '', 'home_url' => '', 'connected_at' => 0, 'safe_mode' => 0, 'last_event_at' => 0, ) ); QuickSync_Logger::log( 'Disconnected QuickSync.' ); wp_safe_redirect( self::settings_url() ); exit; } public static function ajax_handoff_status() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error(); } check_ajax_referer( 'quicksync_handoff', 'nonce' ); $handoff_id = get_transient( 'quicksync_handoff_id' ); $secret = get_transient( 'quicksync_handoff_secret' ); if ( ! $handoff_id || ! $secret ) { wp_send_json_success( array( 'connected' => false, 'expired' => true ) ); } $response = wp_remote_get( QUICKSYNC_API_BASE . '/store/woocommerce/plugin/handoff/' . rawurlencode( $handoff_id ) . '/', array( 'timeout' => 10, 'headers' => array( 'X-QuickSync-Handoff-Secret' => $secret, ), ) ); if ( is_wp_error( $response ) ) { wp_send_json_success( array( 'connected' => false ) ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! empty( $body['connected'] ) && ! empty( $body['token'] ) ) { $pending = (int) get_transient( 'quicksync_pending_key_id' ); self::store_credentials( $body, $pending ? $pending : QuickSync_Plugin::options()['key_id'] ); delete_transient( 'quicksync_handoff_id' ); delete_transient( 'quicksync_handoff_secret' ); delete_transient( 'quicksync_connect_url' ); delete_transient( 'quicksync_handoff_started' ); wp_send_json_success( array( 'connected' => true ) ); } wp_send_json_success( array( 'connected' => false ) ); } public static function expire_unconfirmed_key( $key_id ) { $key_id = absint( $key_id ); if ( $key_id < 1 ) { return; } if ( QuickSync_Plugin::is_connected() && (string) QuickSync_Plugin::options()['key_id'] === (string) $key_id ) { return; } self::revoke_key( $key_id ); } private static function create_handoff() { $replace_id = get_transient( 'quicksync_handoff_id' ); $replace_secret = get_transient( 'quicksync_handoff_secret' ); $key = self::provision_key(); if ( is_wp_error( $key ) ) { return $key; } $headers = array( 'Content-Type' => 'application/json' ); $body = array( 'home_url' => QuickSync_Plugin::current_home_url(), 'consumer_key' => $key['consumer_key'], 'consumer_secret' => $key['consumer_secret'], 'key_id' => (string) $key['key_id'], 'version' => QUICKSYNC_VERSION, ); if ( $replace_id && $replace_secret ) { $headers['X-QuickSync-Handoff-Secret'] = $replace_secret; $body['replace_handoff_id'] = $replace_id; } $response = wp_remote_post( QUICKSYNC_API_BASE . '/store/woocommerce/plugin/handoff/', array( 'timeout' => 15, 'headers' => $headers, 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { self::revoke_key( $key['key_id'] ); return $response; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $body['connect_url'] ) || empty( $body['handoff_id'] ) || empty( $body['poll_secret'] ) ) { self::revoke_key( $key['key_id'] ); $message = ! empty( $body['error'] ) ? $body['error'] : __( 'QuickSync did not accept the connect request.', 'quicksync-pro-for-woocommerce' ); return new WP_Error( 'quicksync_handoff', $message ); } set_transient( 'quicksync_handoff_id', sanitize_text_field( $body['handoff_id'] ), 15 * MINUTE_IN_SECONDS ); set_transient( 'quicksync_handoff_secret', sanitize_text_field( $body['poll_secret'] ), 15 * MINUTE_IN_SECONDS ); set_transient( 'quicksync_connect_url', esc_url_raw( $body['connect_url'] ), 15 * MINUTE_IN_SECONDS ); set_transient( 'quicksync_handoff_started', time(), 15 * MINUTE_IN_SECONDS ); set_transient( 'quicksync_pending_key_id', (int) $key['key_id'], 15 * MINUTE_IN_SECONDS ); QuickSync_Plugin::update_options( array( 'home_url' => QuickSync_Plugin::current_home_url(), ) ); wp_clear_scheduled_hook( 'quicksync_expire_unconfirmed_key' ); wp_schedule_single_event( time() + ( 15 * MINUTE_IN_SECONDS ), 'quicksync_expire_unconfirmed_key', array( (int) $key['key_id'] ) ); QuickSync_Logger::log( 'Opened QuickSync connect handoff ' . $body['handoff_id'] ); return array( 'connect_url' => $body['connect_url'], 'handoff_id' => $body['handoff_id'], ); } private static function store_credentials( $body, $key_id ) { $live = QuickSync_Plugin::options()['key_id']; if ( $live && (string) $live !== (string) $key_id ) { self::revoke_key( $live ); } delete_transient( 'quicksync_pending_key_id' ); QuickSync_Plugin::update_options( array( 'token' => sanitize_text_field( $body['token'] ), 'hmac_secret' => sanitize_text_field( $body['hmac_secret'] ), 'store_id' => sanitize_text_field( isset( $body['store_id'] ) ? $body['store_id'] : '' ), 'key_id' => (string) $key_id, 'home_url' => QuickSync_Plugin::current_home_url(), 'connected_at' => time(), 'safe_mode' => 0, ) ); wp_clear_scheduled_hook( 'quicksync_expire_unconfirmed_key' ); QuickSync_Logger::log( 'Connected to QuickSync.' ); } public static function revoke_stored_keys() { self::revoke_key( QuickSync_Plugin::options()['key_id'] ); $pending = (int) get_transient( 'quicksync_pending_key_id' ); if ( $pending > 0 ) { self::revoke_key( $pending ); delete_transient( 'quicksync_pending_key_id' ); } } private static function revoke_key( $key_id ) { $key_id = absint( $key_id ); if ( $key_id < 1 ) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- WooCommerce REST keys have no public delete-by-id API. $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) ); } private static function provision_key() { if ( ! function_exists( 'wc_api_hash' ) ) { return new WP_Error( 'quicksync_no_wc', __( 'WooCommerce is required to create a REST key.', 'quicksync-pro-for-woocommerce' ) ); } $pending = (int) get_transient( 'quicksync_pending_key_id' ); if ( $pending > 0 ) { self::revoke_key( $pending ); delete_transient( 'quicksync_pending_key_id' ); } global $wpdb; $user_id = get_current_user_id(); $consumer_key = 'ck_' . wc_rand_hash(); $consumer_secret = 'cs_' . wc_rand_hash(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- WooCommerce REST keys have no public insert API. $inserted = $wpdb->insert( $wpdb->prefix . 'woocommerce_api_keys', array( 'user_id' => $user_id, 'description' => 'QuickSync for WooCommerce', 'permissions' => 'read_write', 'consumer_key' => wc_api_hash( $consumer_key ), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr( $consumer_key, -7 ), ), array( '%d', '%s', '%s', '%s', '%s', '%s' ) ); if ( ! $inserted ) { return new WP_Error( 'quicksync_key', __( 'Could not create a WooCommerce REST key.', 'quicksync-pro-for-woocommerce' ) ); } return array( 'key_id' => $wpdb->insert_id, 'consumer_key' => $consumer_key, 'consumer_secret' => $consumer_secret, ); } public static function settings_url() { return admin_url( 'admin.php?page=quicksync-for-woocommerce' ); } }