Changeset 3491681
- Timestamp:
- 03/26/2026 10:36:52 AM (8 days ago)
- Location:
- payment-gateway-for-authorize-net-for-woocommerce
- Files:
-
- 63 added
- 5 edited
-
tags/1.0.10 (added)
-
tags/1.0.10/LICENSE.txt (added)
-
tags/1.0.10/assets (added)
-
tags/1.0.10/assets/css (added)
-
tags/1.0.10/assets/css/admin.css (added)
-
tags/1.0.10/assets/css/credit-cards (added)
-
tags/1.0.10/assets/css/credit-cards/amex.svg (added)
-
tags/1.0.10/assets/css/credit-cards/diners.svg (added)
-
tags/1.0.10/assets/css/credit-cards/discover.svg (added)
-
tags/1.0.10/assets/css/credit-cards/elo.svg (added)
-
tags/1.0.10/assets/css/credit-cards/hiper.svg (added)
-
tags/1.0.10/assets/css/credit-cards/jcb.svg (added)
-
tags/1.0.10/assets/css/credit-cards/maestro.svg (added)
-
tags/1.0.10/assets/css/credit-cards/mastercard.svg (added)
-
tags/1.0.10/assets/css/credit-cards/visa.svg (added)
-
tags/1.0.10/assets/css/public.css (added)
-
tags/1.0.10/assets/images (added)
-
tags/1.0.10/assets/images/brands (added)
-
tags/1.0.10/assets/images/brands/google-pay.svg (added)
-
tags/1.0.10/assets/images/check-routing-account.png (added)
-
tags/1.0.10/assets/js (added)
-
tags/1.0.10/assets/js/acceptjs-echeck-handler.js (added)
-
tags/1.0.10/assets/js/acceptjs-handler.js (added)
-
tags/1.0.10/assets/js/blocks (added)
-
tags/1.0.10/assets/js/blocks-authorizenet.js (added)
-
tags/1.0.10/assets/js/blocks/authorizenet-card.js (added)
-
tags/1.0.10/assets/js/blocks/authorizenet-echeck.js (added)
-
tags/1.0.10/assets/js/blocks/authorizenet-googlepay.js (added)
-
tags/1.0.10/assets/js/blocks/blocks-common.js (added)
-
tags/1.0.10/assets/js/easyauthnet-authorizenet-admin.js (added)
-
tags/1.0.10/assets/js/easyauthnet-review-ajax.js (added)
-
tags/1.0.10/assets/js/googlepay-express.js (added)
-
tags/1.0.10/assets/js/googlepay-handler.js (added)
-
tags/1.0.10/feedback (added)
-
tags/1.0.10/feedback/css (added)
-
tags/1.0.10/feedback/css/deactivation-feedback-modal.css (added)
-
tags/1.0.10/feedback/deactivation-feedback-form.php (added)
-
tags/1.0.10/feedback/fonts (added)
-
tags/1.0.10/feedback/fonts/icomoon.eot (added)
-
tags/1.0.10/feedback/fonts/icomoon.svg (added)
-
tags/1.0.10/feedback/fonts/icomoon.ttf (added)
-
tags/1.0.10/feedback/fonts/icomoon.woff (added)
-
tags/1.0.10/feedback/js (added)
-
tags/1.0.10/feedback/js/deactivation-feedback-modal.js (added)
-
tags/1.0.10/includes (added)
-
tags/1.0.10/includes/class-api-handler.php (added)
-
tags/1.0.10/includes/class-easy-payment-authorizenet-echeck-gateway.php (added)
-
tags/1.0.10/includes/class-easy-payment-authorizenet-gateway.php (added)
-
tags/1.0.10/includes/class-easy-payment-authorizenet-googlepay-gateway.php (added)
-
tags/1.0.10/includes/class-webhook-handler.php (added)
-
tags/1.0.10/includes/compatibility (added)
-
tags/1.0.10/includes/compatibility/class-block-support.php (added)
-
tags/1.0.10/includes/compatibility/class-easyauthnet-subscription-helper.php (added)
-
tags/1.0.10/includes/compatibility/class-funnelkit-compat.php (added)
-
tags/1.0.10/includes/compatibility/class-funnelkit-upsell-authorizenet.php (added)
-
tags/1.0.10/includes/compatibility/class-preorders-compat.php (added)
-
tags/1.0.10/languages (added)
-
tags/1.0.10/languages/easyauthnet-payment-authorizenet.pot (added)
-
tags/1.0.10/languages/payment-gateway-for-authorize-net-for-woocommerce.pot (added)
-
tags/1.0.10/payment-gateway-for-authorizenet-for-woocommerce-admin.php (added)
-
tags/1.0.10/payment-gateway-for-authorizenet-for-woocommerce.php (added)
-
tags/1.0.10/readme.txt (added)
-
tags/1.0.10/uninstall.php (added)
-
trunk/includes/class-api-handler.php (modified) (7 diffs)
-
trunk/includes/class-easy-payment-authorizenet-gateway.php (modified) (5 diffs)
-
trunk/includes/class-webhook-handler.php (modified) (6 diffs)
-
trunk/payment-gateway-for-authorizenet-for-woocommerce.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-api-handler.php
r3473557 r3491681 903 903 $meta_key = EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_' . $env; 904 904 update_user_meta($user_id, $meta_key, $dup_id); 905 906 $validation_list = $response['validationDirectResponseList'] ?? []; 907 $token_consumed = !empty($validation_list) && $validation_list !== []; 908 905 909 self::log(__METHOD__, 'Duplicate profile detected; reusing existing profile id', [ 906 910 'user_id' => $user_id, 907 911 'profile_id' => $dup_id, 908 912 'env' => $env, 913 'token_consumed' => $token_consumed, 909 914 ]); 910 915 return [ … … 921 926 ], 922 927 ], 923 'is_duplicate' => true, 928 'is_duplicate' => true, 929 'token_consumed' => $token_consumed, 924 930 ]; 925 931 } … … 1436 1442 } 1437 1443 1444 /** 1445 * Full list of Authorize.Net event types this plugin handles. 1446 * Any change here is automatically applied to existing webhook registrations 1447 * via update_webhook_event_types_if_needed(). 1448 */ 1449 protected static function get_required_webhook_event_types(): array { 1450 return [ 1451 'net.authorize.payment.authcapture.created', 1452 'net.authorize.payment.authorization.created', 1453 'net.authorize.payment.priorAuthCapture.created', 1454 'net.authorize.payment.void.created', 1455 'net.authorize.payment.refund.created', 1456 'net.authorize.customer.deleted', 1457 'net.authorize.customer.paymentProfile.deleted', 1458 ]; 1459 } 1460 1438 1461 public static function register_webhook() { 1439 1462 self::init_settings(); … … 1452 1475 self::log(__METHOD__, 'Checking existing webhook', ['option_key' => $option_key, 'existing_webhook_id' => $existing_webhook_id]); 1453 1476 if ($existing_webhook_id) { 1454 self::log(__METHOD__, 'Webhook already registered — skipping registration', ['webhook_id' => $existing_webhook_id]); 1477 // Webhook already registered — ensure event types are up to date. 1478 self::update_webhook_event_types_if_needed($existing_webhook_id, $is_sandbox); 1455 1479 return; 1456 1480 } … … 1459 1483 $auth_header = 'Basic ' . base64_encode("{$api_login_id}:{$transaction_key}"); 1460 1484 $body = [ 1461 'url' => $webhook_url, 1462 'eventTypes' => [ 1463 'net.authorize.payment.authcapture.created', 1464 'net.authorize.payment.authorization.created', 1465 'net.authorize.payment.priorAuthCapture.created', 1466 'net.authorize.payment.void.created', 1467 'net.authorize.payment.refund.created', 1468 ], 1469 'status' => 'active', 1485 'url' => $webhook_url, 1486 'eventTypes' => self::get_required_webhook_event_types(), 1487 'status' => 'active', 1470 1488 ]; 1471 1489 self::log(__METHOD__, 'Prepared REST request', ['endpoint' => $endpoint, 'webhook_url' => $webhook_url, 'event_types' => $body['eventTypes']]); … … 1495 1513 } catch (Exception $e) { 1496 1514 self::log(__METHOD__, 'Webhook registration failed', ['message' => $e->getMessage()]); 1515 } 1516 } 1517 1518 /** 1519 * Update an existing Authorize.Net webhook subscription if its event-type 1520 * list differs from get_required_webhook_event_types(). This ensures that 1521 * sites which registered the webhook with an older version of the plugin 1522 * automatically receive any newly required events on the next settings save. 1523 */ 1524 protected static function update_webhook_event_types_if_needed(string $webhook_id, bool $is_sandbox): void { 1525 $api_login_id = self::$api_login_id; 1526 $transaction_key = self::$transaction_key; 1527 $base_endpoint = $is_sandbox ? 'https://apitest.authorize.net/rest/v1/webhooks' : 'https://api.authorize.net/rest/v1/webhooks'; 1528 $auth_header = 'Basic ' . base64_encode("{$api_login_id}:{$transaction_key}"); 1529 1530 // Fetch the current webhook definition. 1531 try { 1532 $get_response = wp_remote_get("{$base_endpoint}/{$webhook_id}", [ 1533 'headers' => [ 1534 'Content-Type' => 'application/json', 1535 'Authorization' => $auth_header, 1536 'User-Agent' => 'WooCommerce-Easy-Payment-AuthorizeNet/' . EASYAUTHNET_AUTHORIZENET_VERSION, 1537 ], 1538 'timeout' => 15, 1539 ]); 1540 1541 if (is_wp_error($get_response)) { 1542 throw new Exception($get_response->get_error_message()); 1543 } 1544 1545 $get_http_code = (int) wp_remote_retrieve_response_code($get_response); 1546 $current = json_decode(wp_remote_retrieve_body($get_response), true); 1547 1548 // If the webhook no longer exists on Authorize.Net's side (404 or missing webhookId), 1549 // clear the stored option so register_webhook() creates a fresh one next time. 1550 if ($get_http_code !== 200 || empty($current['webhookId'])) { 1551 $option_key = $is_sandbox ? 'easyauthnet_anet_webhook_id_sandbox' : 'easyauthnet_anet_webhook_id_live'; 1552 delete_option($option_key); 1553 self::log(__METHOD__, 'Stored webhook no longer exists in Authorize.Net — cleared option for re-registration', [ 1554 'webhook_id' => $webhook_id, 1555 'http_status' => $get_http_code, 1556 'option_key' => $option_key, 1557 ]); 1558 return; 1559 } 1560 1561 $current_types = isset($current['eventTypes']) && is_array($current['eventTypes']) 1562 ? $current['eventTypes'] 1563 : []; 1564 1565 $required_types = self::get_required_webhook_event_types(); 1566 1567 $missing = array_diff($required_types, $current_types); 1568 1569 if (empty($missing)) { 1570 self::log(__METHOD__, 'Webhook event types are already up to date', ['webhook_id' => $webhook_id]); 1571 return; 1572 } 1573 1574 self::log(__METHOD__, 'Updating webhook with missing event types', [ 1575 'webhook_id' => $webhook_id, 1576 'missing_types' => array_values($missing), 1577 ]); 1578 1579 // PATCH the existing webhook with the full required list. 1580 $patch_response = wp_remote_request("{$base_endpoint}/{$webhook_id}", [ 1581 'method' => 'PUT', 1582 'headers' => [ 1583 'Content-Type' => 'application/json', 1584 'Authorization' => $auth_header, 1585 'User-Agent' => 'WooCommerce-Easy-Payment-AuthorizeNet/' . EASYAUTHNET_AUTHORIZENET_VERSION, 1586 ], 1587 'body' => wp_json_encode([ 1588 'url' => isset($current['url']) ? $current['url'] : home_url('/wp-json/easyauthnet-authorizenet/v1/webhook'), 1589 'eventTypes' => $required_types, 1590 'status' => 'active', 1591 ]), 1592 'timeout' => 20, 1593 ]); 1594 1595 if (is_wp_error($patch_response)) { 1596 throw new Exception($patch_response->get_error_message()); 1597 } 1598 1599 $http_code = wp_remote_retrieve_response_code($patch_response); 1600 self::log(__METHOD__, 'Webhook update response', [ 1601 'webhook_id' => $webhook_id, 1602 'http_status' => $http_code, 1603 ]); 1604 } catch (Exception $e) { 1605 self::log(__METHOD__, 'Failed to update webhook event types', [ 1606 'webhook_id' => $webhook_id, 1607 'message' => $e->getMessage(), 1608 ]); 1497 1609 } 1498 1610 } … … 1548 1660 self::log(__METHOD__, 'Exception during profile validation', ['exception' => get_class($e), 'message' => $e->getMessage(), 'profile_id' => $customer_profile_id]); 1549 1661 return false; 1662 } 1663 } 1664 1665 public static function delete_customer_profile_from_cim($customer_profile_id) { 1666 self::init_settings(); 1667 1668 $endpoint = self::get_api_endpoint(); 1669 self::log(__METHOD__, 'Deleting customer profile from CIM', [ 1670 'customer_profile_id' => $customer_profile_id, 1671 ]); 1672 1673 try { 1674 $request = [ 1675 'root_element' => 'deleteCustomerProfileRequest', 1676 'merchantAuthentication' => [ 1677 'name' => self::$api_login_id, 1678 'transactionKey' => self::$transaction_key, 1679 ], 1680 'customerProfileId' => $customer_profile_id, 1681 ]; 1682 1683 $response = self::send_request($endpoint, $request); 1684 $result_code = $response['messages']['resultCode'] ?? ''; 1685 $message_code = $response['messages']['message']['code'] ?? ''; 1686 $message_text = $response['messages']['message']['text'] ?? ''; 1687 1688 // E00040 = record not found — treat as success (already deleted on their side). 1689 if ($result_code === 'Ok' || $message_code === 'E00040') { 1690 self::log(__METHOD__, 'Customer profile delete treated as success', [ 1691 'customer_profile_id' => $customer_profile_id, 1692 'message_code' => $message_code ?: 'I00001', 1693 'message_text' => $message_text, 1694 ]); 1695 return true; 1696 } 1697 1698 self::log(__METHOD__, 'Customer profile delete failed', [ 1699 'customer_profile_id' => $customer_profile_id, 1700 'result_code' => $result_code ?: 'none', 1701 'message_code' => $message_code ?: 'none', 1702 'message_text' => $message_text ?: __('Unknown error.', 'payment-gateway-for-authorize-net-for-woocommerce'), 1703 ]); 1704 1705 return new WP_Error( 1706 'easyauthnet_delete_customer_profile_failed', 1707 $message_text ?: __('Failed to delete customer profile from Authorize.Net CIM.', 'payment-gateway-for-authorize-net-for-woocommerce'), 1708 [ 1709 'customer_profile_id' => $customer_profile_id, 1710 'response' => $response, 1711 ] 1712 ); 1713 } catch (Exception $e) { 1714 self::log(__METHOD__, 'Exception deleting customer profile', [ 1715 'customer_profile_id' => $customer_profile_id, 1716 'exception' => get_class($e), 1717 'message' => $e->getMessage(), 1718 ]); 1719 1720 return new WP_Error('easyauthnet_delete_customer_profile_exception', $e->getMessage()); 1721 } 1722 } 1723 1724 public static function delete_customer_payment_profile_from_cim($customer_profile_id, $payment_profile_id) { 1725 self::init_settings(); 1726 1727 $endpoint = self::get_api_endpoint(); 1728 self::log(__METHOD__, 'Deleting customer payment profile from CIM', [ 1729 'customer_profile_id' => $customer_profile_id, 1730 'payment_profile_id' => $payment_profile_id, 1731 ]); 1732 1733 try { 1734 $request = [ 1735 'root_element' => 'deleteCustomerPaymentProfileRequest', 1736 'merchantAuthentication' => [ 1737 'name' => self::$api_login_id, 1738 'transactionKey' => self::$transaction_key, 1739 ], 1740 'customerProfileId' => $customer_profile_id, 1741 'customerPaymentProfileId' => $payment_profile_id, 1742 ]; 1743 1744 $response = self::send_request($endpoint, $request); 1745 $result_code = $response['messages']['resultCode'] ?? ''; 1746 $message_code = $response['messages']['message']['code'] ?? ''; 1747 $message_text = $response['messages']['message']['text'] ?? ''; 1748 1749 if ($result_code === 'Ok' || $message_code === 'E00040') { 1750 self::log(__METHOD__, 'Customer payment profile delete treated as success', [ 1751 'customer_profile_id' => $customer_profile_id, 1752 'payment_profile_id' => $payment_profile_id, 1753 'message_code' => $message_code ?: 'I00001', 1754 'message_text' => $message_text, 1755 ]); 1756 return true; 1757 } 1758 1759 self::log(__METHOD__, 'Customer payment profile delete failed', [ 1760 'customer_profile_id' => $customer_profile_id, 1761 'payment_profile_id' => $payment_profile_id, 1762 'result_code' => $result_code ?: 'none', 1763 'message_code' => $message_code ?: 'none', 1764 'message_text' => $message_text ?: __('Unknown error.', 'payment-gateway-for-authorize-net-for-woocommerce'), 1765 ]); 1766 1767 return new WP_Error( 1768 'easyauthnet_delete_payment_profile_failed', 1769 $message_text ?: __('Failed to delete payment profile from Authorize.Net CIM.', 'payment-gateway-for-authorize-net-for-woocommerce'), 1770 [ 1771 'customer_profile_id' => $customer_profile_id, 1772 'payment_profile_id' => $payment_profile_id, 1773 'response' => $response, 1774 ] 1775 ); 1776 } catch (Exception $e) { 1777 self::log(__METHOD__, 'Exception deleting customer payment profile', [ 1778 'customer_profile_id' => $customer_profile_id, 1779 'payment_profile_id' => $payment_profile_id, 1780 'exception' => get_class($e), 1781 'message' => $e->getMessage(), 1782 ]); 1783 1784 return new WP_Error('easyauthnet_delete_payment_profile_exception', $e->getMessage()); 1785 } 1786 } 1787 1788 public static function validate_customer_payment_profile($customer_profile_id, $payment_profile_id) { 1789 self::init_settings(); 1790 1791 $endpoint = self::get_api_endpoint(); 1792 self::log(__METHOD__, 'Validating customer payment profile', [ 1793 'customer_profile_id' => $customer_profile_id, 1794 'payment_profile_id' => $payment_profile_id, 1795 ]); 1796 1797 try { 1798 $request = [ 1799 'root_element' => 'getCustomerPaymentProfileRequest', 1800 'merchantAuthentication' => [ 1801 'name' => self::$api_login_id, 1802 'transactionKey' => self::$transaction_key, 1803 ], 1804 'customerProfileId' => $customer_profile_id, 1805 'customerPaymentProfileId' => $payment_profile_id, 1806 ]; 1807 1808 $response = self::send_request($endpoint, $request); 1809 $result_code = $response['messages']['resultCode'] ?? ''; 1810 $message_code = $response['messages']['message']['code'] ?? ''; 1811 1812 if ($result_code === 'Ok') { 1813 self::log(__METHOD__, 'Customer payment profile exists', [ 1814 'customer_profile_id' => $customer_profile_id, 1815 'payment_profile_id' => $payment_profile_id, 1816 ]); 1817 return true; 1818 } 1819 1820 if ($message_code === 'E00040') { 1821 self::log(__METHOD__, 'Customer payment profile not found', [ 1822 'customer_profile_id' => $customer_profile_id, 1823 'payment_profile_id' => $payment_profile_id, 1824 ]); 1825 return false; 1826 } 1827 1828 self::log(__METHOD__, 'Customer payment profile validation failed open after API response', [ 1829 'customer_profile_id' => $customer_profile_id, 1830 'payment_profile_id' => $payment_profile_id, 1831 'result_code' => $result_code ?: 'none', 1832 'message_code' => $message_code ?: 'none', 1833 'message_text' => $response['messages']['message']['text'] ?? '', 1834 ]); 1835 1836 return true; 1837 } catch (Exception $e) { 1838 self::log(__METHOD__, 'Exception validating customer payment profile - failing open', [ 1839 'customer_profile_id' => $customer_profile_id, 1840 'payment_profile_id' => $payment_profile_id, 1841 'exception' => get_class($e), 1842 'message' => $e->getMessage(), 1843 ]); 1844 1845 return true; 1550 1846 } 1551 1847 } -
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-easy-payment-authorizenet-gateway.php
r3473557 r3491681 29 29 public $customer_profile_id; 30 30 private static $easyauthnet_notice_rendered = false; 31 private static $cim_skip_token_ids = []; 32 33 /** 34 * Mark a token ID so that the woocommerce_payment_token_deleted hook will 35 * skip the CIM deletion for it. Used by the webhook handler when a profile 36 * has already been removed on the Authorize.Net side. 37 */ 38 public static function skip_cim_sync_for_token(int $token_id): void { 39 self::$cim_skip_token_ids[$token_id] = true; 40 } 41 42 public static function unskip_cim_sync_for_token(int $token_id): void { 43 unset(self::$cim_skip_token_ids[$token_id]); 44 } 31 45 32 46 protected const CUSTOMER_PROFILE_MIGRATION_FLAG = 'easyauthnet_authorizenet_profile_migration_v1'; … … 173 187 add_filter('safe_style_css', array($this, 'easyauthnet_allowed_css_properties')); 174 188 add_action('admin_notices', [$this, 'easyauthnet_cc_missing_creds_notice']); 189 add_action('woocommerce_payment_token_deleted', [$this, 'easyauthnet_sync_delete_cim_on_token_removed'], 10, 2); 190 add_action('woocommerce_account_payment_methods_endpoint', [$this, 'easyauthnet_prune_stale_tokens_on_account_page']); 191 add_action('before_delete_user', [$this, 'easyauthnet_delete_cim_profile_on_user_deleted']); 175 192 } 176 193 … … 1174 1191 $this->log("Failed to save payment token", ['error_code' => $error_code, 'error_message' => $token->get_error_message()]); 1175 1192 1193 if ($error_code === 'easyauthnet_ots_token_consumed') { 1194 $this->log('OTS token was consumed by duplicate-profile check; direct charge not possible — asking customer to retry', [ 1195 'order_id' => $order->get_id(), 1196 ]); 1197 wc_add_notice($token->get_error_message(), 'error'); 1198 return ['result' => 'failure']; 1199 } 1200 1176 1201 // Graceful fallback: intermittent Authorize.Net E00114 (Invalid OTS Token). 1177 1202 // In this scenario, the CIM save fails but we can still charge the order using opaque data. … … 1570 1595 ]); 1571 1596 1597 if (!empty($create_result['is_duplicate']) && !empty($create_result['token_consumed'])) { 1598 if ($user_id > 0) { 1599 update_user_meta($user_id, $this->customer_profile_id, $customer_profile_id); 1600 } 1601 if ($order instanceof WC_Order) { 1602 $order->update_meta_data('_easyauthnet_authorizenet_customer_profile_id', $customer_profile_id); 1603 $order->save(); 1604 } 1605 1606 1607 if (!empty($create_result['customerPaymentProfileIdList'])) { 1608 $list = $create_result['customerPaymentProfileIdList']; 1609 $candidate = ''; 1610 if (is_array($list)) { 1611 if (isset($list['numericString'])) { 1612 $ns = $list['numericString']; 1613 $candidate = is_array($ns) ? (string) reset($ns) : (string) $ns; 1614 } else { 1615 $candidate = (string) reset($list); 1616 } 1617 } else { 1618 $candidate = (string) $list; 1619 } 1620 if ($candidate !== '') { 1621 $payment_profile_id_from_create = $candidate; 1622 $this->log('OTS token consumed by duplicate-profile; reusing payment profile ID returned in response', [ 1623 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1624 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id_from_create), 1625 ]); 1626 // Fall through — $payment_profile_id_from_create will be used below. 1627 } 1628 } 1629 1630 if (empty($payment_profile_id_from_create)) { 1631 $this->log('OTS token consumed by duplicate-profile validation; cannot reuse for payment profile creation', [ 1632 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1633 ]); 1634 return new WP_Error( 1635 'easyauthnet_ots_token_consumed', 1636 __('Your payment session has expired. Please try again.', 'payment-gateway-for-authorize-net-for-woocommerce'), 1637 ['customer_profile_id' => $customer_profile_id] 1638 ); 1639 } 1640 1641 } 1572 1642 if (!empty($create_result['customerPaymentProfileIdList'])) { 1573 1643 $list = $create_result['customerPaymentProfileIdList']; … … 1664 1734 } 1665 1735 1736 public function easyauthnet_sync_delete_cim_on_token_removed($token_id, $token) { 1737 if (!$token instanceof WC_Payment_Token) { 1738 $this->log('Skipping CIM delete for invalid token instance', ['token_id' => $token_id]); 1739 return; 1740 } 1741 1742 if ($token->get_gateway_id() !== $this->id) { 1743 return; 1744 } 1745 1746 if (!empty(self::$cim_skip_token_ids[$token_id])) { 1747 $this->log('Skipping CIM delete for internally pruned token', ['token_id' => $token_id]); 1748 return; 1749 } 1750 1751 $customer_profile_id = (string) $token->get_meta('customer_profile_id'); 1752 $payment_profile_id = (string) $token->get_meta('payment_profile_id'); 1753 if ($payment_profile_id === '') { 1754 $payment_profile_id = (string) $token->get_token(); 1755 } 1756 1757 if ($customer_profile_id === '' || $payment_profile_id === '') { 1758 $this->log('Skipping CIM delete for legacy or incomplete token', [ 1759 'token_id' => $token_id, 1760 'has_customer_profile_id' => $customer_profile_id !== '', 1761 'has_payment_profile_id' => $payment_profile_id !== '', 1762 ]); 1763 return; 1764 } 1765 1766 $result = EASYAUTHNET_AuthorizeNet_API_Handler::delete_customer_payment_profile_from_cim($customer_profile_id, $payment_profile_id); 1767 if (is_wp_error($result)) { 1768 $this->log('Failed deleting CIM payment profile after Woo token removal', [ 1769 'token_id' => $token_id, 1770 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1771 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id), 1772 'error_code' => $result->get_error_code(), 1773 'error_message' => $result->get_error_message(), 1774 ]); 1775 return; 1776 } 1777 1778 $this->log('Deleted CIM payment profile after Woo token removal', [ 1779 'token_id' => $token_id, 1780 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1781 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id), 1782 ]); 1783 1784 $this->maybe_delete_customer_profile_meta_if_unused($token->get_user_id(), $customer_profile_id); 1785 } 1786 1787 public function easyauthnet_delete_cim_profile_on_user_deleted($user_id) { 1788 $user_id = (int) $user_id; 1789 if ($user_id <= 0) { 1790 return; 1791 } 1792 1793 $customer_profile_id = (string) get_user_meta($user_id, $this->customer_profile_id, true); 1794 if ($customer_profile_id === '') { 1795 return; 1796 } 1797 1798 $result = EASYAUTHNET_AuthorizeNet_API_Handler::delete_customer_profile_from_cim($customer_profile_id); 1799 if (is_wp_error($result)) { 1800 $this->log('Failed deleting CIM customer profile after WP user deletion', [ 1801 'user_id' => $user_id, 1802 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1803 'error_code' => $result->get_error_code(), 1804 'error_message' => $result->get_error_message(), 1805 ]); 1806 return; 1807 } 1808 1809 $this->log('Deleted CIM customer profile after WP user deletion', [ 1810 'user_id' => $user_id, 1811 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1812 ]); 1813 } 1814 1815 public function easyauthnet_prune_stale_tokens_on_account_page() { 1816 if (!is_user_logged_in()) { 1817 return; 1818 } 1819 1820 $user_id = get_current_user_id(); 1821 $tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id); 1822 if (empty($tokens)) { 1823 return; 1824 } 1825 1826 foreach ($tokens as $token) { 1827 if (!$token instanceof WC_Payment_Token) { 1828 continue; 1829 } 1830 1831 $token_id = $token->get_id(); 1832 $customer_profile_id = (string) $token->get_meta('customer_profile_id'); 1833 $payment_profile_id = (string) $token->get_meta('payment_profile_id'); 1834 if ($payment_profile_id === '') { 1835 $payment_profile_id = (string) $token->get_token(); 1836 } 1837 1838 if ($customer_profile_id === '' || $payment_profile_id === '') { 1839 $this->log('Skipping stale token prune for legacy or incomplete token', [ 1840 'token_id' => $token_id, 1841 'has_customer_profile_id' => $customer_profile_id !== '', 1842 'has_payment_profile_id' => $payment_profile_id !== '', 1843 ]); 1844 continue; 1845 } 1846 1847 $exists = EASYAUTHNET_AuthorizeNet_API_Handler::validate_customer_payment_profile($customer_profile_id, $payment_profile_id); 1848 if ($exists) { 1849 continue; 1850 } 1851 1852 $this->log('Pruning stale Woo token missing in CIM', [ 1853 'token_id' => $token_id, 1854 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1855 'payment_profile_id' => $this->mask_sensitive_data($payment_profile_id), 1856 ]); 1857 1858 self::$cim_skip_token_ids[$token_id] = true; 1859 WC_Payment_Tokens::delete($token_id); 1860 unset(self::$cim_skip_token_ids[$token_id]); 1861 1862 $this->maybe_delete_customer_profile_meta_if_unused($user_id, $customer_profile_id); 1863 } 1864 } 1865 1866 protected function maybe_delete_customer_profile_meta_if_unused($user_id, $customer_profile_id) { 1867 $user_id = (int) $user_id; 1868 if ($user_id <= 0 || $customer_profile_id === '') { 1869 return; 1870 } 1871 1872 $tokens = WC_Payment_Tokens::get_customer_tokens($user_id, $this->id); 1873 foreach ($tokens as $token) { 1874 if (!$token instanceof WC_Payment_Token) { 1875 continue; 1876 } 1877 1878 if ((string) $token->get_meta('customer_profile_id') === $customer_profile_id) { 1879 return; 1880 } 1881 } 1882 1883 delete_user_meta($user_id, $this->customer_profile_id); 1884 $this->log('Deleted stored CIM customer profile meta after last token removal', [ 1885 'user_id' => $user_id, 1886 'customer_profile_id' => $this->mask_sensitive_data($customer_profile_id), 1887 ]); 1888 } 1889 1666 1890 public function process_refund($order_id, $amount = null, $reason = '') { 1667 1891 try { -
payment-gateway-for-authorize-net-for-woocommerce/trunk/includes/class-webhook-handler.php
r3462451 r3491681 19 19 $status = str_replace('wc-', '', $status); 20 20 return $status !== '' ? $status : 'on-hold'; 21 } 22 23 protected static function force_delete_token_rows(int $token_id): void { 24 if ($token_id <= 0) { 25 return; 26 } 27 28 global $wpdb; 29 30 $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 31 "{$wpdb->prefix}woocommerce_payment_tokenmeta", 32 ['payment_token_id' => $token_id], 33 ['%d'] 34 ); 35 36 $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 37 "{$wpdb->prefix}woocommerce_payment_tokens", 38 ['token_id' => $token_id], 39 ['%d'] 40 ); 21 41 } 22 42 … … 27 47 self::$transaction_type = isset($settings['transaction_type']) ? sanitize_text_field($settings['transaction_type']) : 'auth_capture'; 28 48 29 // Note: Raw body must be read unmodified for signature verification.30 49 $raw_body = file_get_contents('php://input'); 31 50 … … 50 69 ); 51 70 52 // Sanitize signature header 53 $signature_header = isset($_SERVER['HTTP_X_ANET_SIGNATURE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_ANET_SIGNATURE'])) : ''; 54 55 // Validate structure 71 $signature_header = isset($_SERVER['HTTP_X_ANET_SIGNATURE']) ? wp_unslash($_SERVER['HTTP_X_ANET_SIGNATURE']) : ''; 72 56 73 if (empty($data['eventType']) || empty($data['payload']) || empty($signature_header)) { 57 74 return new WP_REST_Response(['status' => 'invalid_payload_or_signature'], 400); 58 75 } 59 76 60 // Verify signature61 77 if (!self::verify_signature($raw_body, $signature_header)) { 62 78 self::log('Signature mismatch — webhook rejected', [], 'warning'); … … 80 96 self::handle_refund($payload); 81 97 break; 98 case 'net.authorize.customer.deleted': 99 self::handle_customer_deleted($payload); 100 break; 101 case 'net.authorize.customer.paymentProfile.deleted': 102 self::handle_payment_profile_deleted($payload); 103 break; 82 104 default: 83 105 do_action('easyauthnet_authorizenet_webhook_' . $event_type, $payload); … … 138 160 if (in_array($current_status, ['pending', 'on-hold', 'processing'], true)) { 139 161 if (abs($capture_amount - $order_total) < 0.01) { 140 // For eCheck (ACH), merchants often want to keep the order on-hold until they confirm settlement.141 162 $new_status = $is_echeck ? $echeck_webhook_status : 'completed'; 142 163 $order->update_status($new_status); … … 294 315 } 295 316 317 protected static function handle_customer_deleted($payload) { 318 $customer_profile_id = sanitize_text_field($payload['id'] ?? $payload['customerProfileId'] ?? $payload['profileId'] ?? ''); 319 320 if (empty($customer_profile_id)) { 321 self::log('Customer deleted webhook: missing customer profile ID in payload', [], 'warning'); 322 return; 323 } 324 325 self::log('Customer deleted webhook received', ['customer_profile_id' => $customer_profile_id]); 326 327 $user_ids_live = get_users([ 328 'meta_key' => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live', 329 'meta_value' => $customer_profile_id, 330 'fields' => 'ID', 331 'number' => -1, 332 ]); 333 $user_ids_sandbox = get_users([ 334 'meta_key' => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox', 335 'meta_value' => $customer_profile_id, 336 'fields' => 'ID', 337 'number' => -1, 338 ]); 339 340 $user_ids = array_values(array_unique(array_merge( 341 array_map('intval', (array) $user_ids_live), 342 array_map('intval', (array) $user_ids_sandbox) 343 ))); 344 345 $token_map = []; 346 $affected_user_ids = []; 347 348 foreach ($user_ids as $user_id) { 349 if ($user_id <= 0) { 350 continue; 351 } 352 $affected_user_ids[] = $user_id; 353 354 $user_tokens = WC_Payment_Tokens::get_customer_tokens($user_id, 'easyauthnet_authorizenet'); 355 foreach ($user_tokens as $token) { 356 if (!$token instanceof WC_Payment_Token) { 357 continue; 358 } 359 if ((string) $token->get_meta('customer_profile_id') !== $customer_profile_id) { 360 continue; 361 } 362 $token_map[(int) $token->get_id()] = $token; 363 } 364 } 365 366 if (empty($token_map)) { 367 self::log('Customer deleted webhook: no local tokens found for profile', ['customer_profile_id' => $customer_profile_id]); 368 } 369 370 foreach ($token_map as $token_id => $token) { 371 $token_id = (int) $token_id; 372 373 if (!$token instanceof WC_Payment_Token) { 374 self::force_delete_token_rows($token_id); 375 self::log('Force-deleted local token rows for customer-deleted webhook (invalid token object)', [ 376 'token_id' => $token_id, 377 'customer_profile_id' => $customer_profile_id, 378 ], 'warning'); 379 continue; 380 } 381 382 $affected_user_ids[] = (int) $token->get_user_id(); 383 384 if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) { 385 EASYAUTHNET_AuthorizeNet_Gateway::skip_cim_sync_for_token($token_id); 386 } 387 388 WC_Payment_Tokens::delete($token_id); 389 390 if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) { 391 EASYAUTHNET_AuthorizeNet_Gateway::unskip_cim_sync_for_token($token_id); 392 } 393 394 self::log('Deleted local token for Authorize.Net customer-deleted webhook', [ 395 'token_id' => $token_id, 396 'customer_profile_id' => $customer_profile_id, 397 ]); 398 } 399 400 foreach (array_unique($affected_user_ids) as $user_id) { 401 delete_user_meta($user_id, EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live'); 402 delete_user_meta($user_id, EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox'); 403 self::log('Deleted customer profile user meta after Authorize.Net customer-deleted webhook', [ 404 'user_id' => $user_id, 405 'customer_profile_id' => $customer_profile_id, 406 ]); 407 } 408 } 409 410 protected static function handle_payment_profile_deleted($payload) { 411 $payment_profile_id = sanitize_text_field($payload['id'] ?? ''); 412 $customer_profile_id = sanitize_text_field($payload['customerProfileId'] ?? $payload['profileId'] ?? ''); 413 414 if (empty($payment_profile_id)) { 415 self::log('Payment profile deleted webhook: missing payment profile ID in payload', [], 'warning'); 416 return; 417 } 418 419 self::log('Payment profile deleted webhook received', [ 420 'payment_profile_id' => $payment_profile_id, 421 'customer_profile_id' => $customer_profile_id, 422 ]); 423 424 $matching_tokens = []; 425 426 if (!empty($customer_profile_id)) { 427 $user_ids_live = get_users([ 428 'meta_key' => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_live', 429 'meta_value' => $customer_profile_id, 430 'fields' => 'ID', 431 'number' => -1, 432 ]); 433 $user_ids_sandbox = get_users([ 434 'meta_key' => EASYAUTHNET_AUTHORIZENET_CUSTOMER_PROFILE_ID . '_sandbox', 435 'meta_value' => $customer_profile_id, 436 'fields' => 'ID', 437 'number' => -1, 438 ]); 439 $user_ids = array_values(array_unique(array_merge( 440 array_map('intval', (array) $user_ids_live), 441 array_map('intval', (array) $user_ids_sandbox) 442 ))); 443 444 foreach ($user_ids as $user_id) { 445 if ($user_id <= 0) { 446 continue; 447 } 448 foreach (WC_Payment_Tokens::get_customer_tokens($user_id, 'easyauthnet_authorizenet') as $token) { 449 if (!$token instanceof WC_Payment_Token) { 450 continue; 451 } 452 if ( 453 (string) $token->get_meta('payment_profile_id') === $payment_profile_id || 454 (string) $token->get_token() === $payment_profile_id 455 ) { 456 $matching_tokens[(int) $token->get_id()] = $token; 457 } 458 } 459 } 460 } else { 461 foreach (WC_Payment_Tokens::get_tokens(['gateway_id' => 'easyauthnet_authorizenet']) as $token) { 462 if (!$token instanceof WC_Payment_Token) { 463 continue; 464 } 465 if ( 466 (string) $token->get_meta('payment_profile_id') === $payment_profile_id || 467 (string) $token->get_token() === $payment_profile_id 468 ) { 469 $matching_tokens[(int) $token->get_id()] = $token; 470 } 471 } 472 } 473 474 if (empty($matching_tokens)) { 475 self::log('Payment profile deleted webhook: no local tokens found for payment profile', [ 476 'payment_profile_id' => $payment_profile_id, 477 ]); 478 return; 479 } 480 481 foreach ($matching_tokens as $token_id => $token) { 482 $token_id = (int) $token_id; 483 $user_id = (int) $token->get_user_id(); 484 485 if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) { 486 EASYAUTHNET_AuthorizeNet_Gateway::skip_cim_sync_for_token($token_id); 487 } 488 489 WC_Payment_Tokens::delete($token_id); 490 491 if (class_exists('EASYAUTHNET_AuthorizeNet_Gateway')) { 492 EASYAUTHNET_AuthorizeNet_Gateway::unskip_cim_sync_for_token($token_id); 493 } 494 495 self::log('Deleted local token for Authorize.Net payment-profile-deleted webhook', [ 496 'token_id' => $token_id, 497 'payment_profile_id' => $payment_profile_id, 498 'customer_profile_id' => $customer_profile_id, 499 ]); 500 } 501 } 502 296 503 protected static function log($message, $context = [], $level = 'info') { 297 504 if (!class_exists('WC_Logger')) { -
payment-gateway-for-authorize-net-for-woocommerce/trunk/payment-gateway-for-authorizenet-for-woocommerce.php
r3473557 r3491681 7 7 * Author: easypayment 8 8 * Author URI: https://profiles.wordpress.org/easypayment/ 9 * Version: 1.0. 99 * Version: 1.0.10 10 10 * Requires at least: 5.6 11 * Tested up to: 6.9. 111 * Tested up to: 6.9.4 12 12 * Requires PHP: 7.4 13 13 * Text Domain: payment-gateway-for-authorize-net-for-woocommerce 14 14 * Domain Path: /languages/ 15 15 * WC requires at least: 6.0 16 * WC tested up to: 10. 5.216 * WC tested up to: 10.6.1 17 17 * Requires Plugins: woocommerce 18 18 * License: GPLv2 or later … … 23 23 24 24 if (!defined('EASYAUTHNET_AUTHORIZENET_VERSION')) { 25 define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0. 9');25 define('EASYAUTHNET_AUTHORIZENET_VERSION', '1.0.10'); 26 26 } 27 27 define('EASYAUTHNET_AUTHORIZENET_PLUGIN_FILE', __FILE__); -
payment-gateway-for-authorize-net-for-woocommerce/trunk/readme.txt
r3473557 r3491681 3 3 Tags: authorize.net, credit card, visa 4 4 Requires at least: 5.6 5 Tested up to: 6.9. 15 Tested up to: 6.9.4 6 6 Requires PHP: 7.4 7 Stable tag: 1.0. 97 Stable tag: 1.0.10 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 121 121 == Changelog == 122 122 123 = 1.0.10 = 124 * Improved - Synchronization between Authorize.net CIM and WooCommerce saved payment methods. 125 123 126 = 1.0.9 = 124 127 * Added - FunnelKit compatible for upsell and cross-sell flows.
Note: See TracChangeset
for help on using the changeset viewer.