PluginProbe ʕ •ᴥ•ʔ
Payment Gateway for Authorize.net for WooCommerce / 1.0.3
Payment Gateway for Authorize.net for WooCommerce v1.0.3
1.0.19 1.0.18 1.0.17 1.0.16 1.0.15 1.0.14 1.0.13 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
payment-gateway-for-authorize-net-for-woocommerce / includes / class-webhook-handler.php
payment-gateway-for-authorize-net-for-woocommerce / includes Last commit date
compatibility 8 months ago class-api-handler.php 8 months ago class-easy-payment-authorizenet-gateway.php 8 months ago class-webhook-handler.php 8 months ago
class-webhook-handler.php
289 lines
1 <?php
2
3 if (!defined('ABSPATH'))
4 exit;
5
6 class EASYAUTHNET_AuthorizeNet_Webhook_Handler {
7
8 protected static $debug_mode = 'no';
9 protected static $environment = 'sandbox';
10 protected static $transaction_type = 'auth_capture';
11
12 public static function handle(WP_REST_Request $request = null) {
13 $settings = get_option('woocommerce_easyauthnet_authorizenet_settings', []);
14 self::$debug_mode = isset($settings['debug']) ? sanitize_text_field($settings['debug']) : 'no';
15 self::$environment = isset($settings['environment']) ? sanitize_text_field($settings['environment']) : 'sandbox';
16 self::$transaction_type = isset($settings['transaction_type']) ? sanitize_text_field($settings['transaction_type']) : 'auth_capture';
17
18 // Note: Raw body must be read unmodified for signature verification.
19 $raw_body = file_get_contents('php://input');
20
21 if (empty($raw_body)) {
22 return new WP_REST_Response(['status' => 'missing_body'], 400);
23 }
24
25 $data = json_decode($raw_body, true);
26
27 if (!is_array($data)) {
28 return new WP_REST_Response(['status' => 'invalid_json'], 400);
29 }
30
31 self::log(
32 'Webhook Raw Data',
33 is_array($data) ? array_map(
34 function ($v) {
35 return is_scalar($v) ? sanitize_text_field(wp_unslash($v)) : $v;
36 },
37 $data
38 ) : []
39 );
40
41 // Sanitize signature header
42 $signature_header = isset($_SERVER['HTTP_X_ANET_SIGNATURE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_X_ANET_SIGNATURE'])) : '';
43
44 // Validate structure
45 if (empty($data['eventType']) || empty($data['payload']) || empty($signature_header)) {
46 return new WP_REST_Response(['status' => 'invalid_payload_or_signature'], 400);
47 }
48
49 // Verify signature
50 if (!self::verify_signature($raw_body, $signature_header)) {
51 self::log('Signature mismatch — webhook rejected', [], 'warning');
52 return new WP_REST_Response(['status' => 'unauthorized'], 401);
53 }
54
55 $event_type = sanitize_text_field($data['eventType']);
56 $payload = is_array($data['payload']) ? $data['payload'] : [];
57
58 self::log("Webhook received: {$event_type}", $payload);
59
60 switch ($event_type) {
61 case 'net.authorize.payment.authcapture.created':
62 case 'net.authorize.payment.priorAuthCapture.created':
63 self::handle_capture($payload);
64 break;
65 case 'net.authorize.payment.void.created':
66 self::handle_void($payload);
67 break;
68 case 'net.authorize.payment.refund.created':
69 self::handle_refund($payload);
70 break;
71 default:
72 do_action('easyauthnet_authorizenet_webhook_' . $event_type, $payload);
73 break;
74 }
75
76 return new WP_REST_Response(['status' => 'ok'], 200);
77 }
78
79 protected static function verify_signature($rawPayload, $receivedSignature) {
80 $settings = get_option('woocommerce_easyauthnet_authorizenet_settings', []);
81 $signatureKey = self::$environment === 'live' ? sanitize_text_field($settings['live_signature_key'] ?? '') : sanitize_text_field($settings['sandbox_signature_key'] ?? '');
82
83 if (empty($signatureKey)) {
84 self::log("Missing signature key for environment: " . self::$environment, [], 'warning');
85 http_response_code(401);
86 return false;
87 }
88
89 $calculatedHash = strtoupper(hash_hmac('sha512', $rawPayload, $signatureKey));
90 $expectedSignature = "sha512={$calculatedHash}";
91
92 self::log("Expected Signature: {$expectedSignature}", [], 'debug');
93 self::log("Received Signature: {$receivedSignature}", [], 'debug');
94
95 if (hash_equals($expectedSignature, $receivedSignature)) {
96 self::log("�
97 Webhook signature verified successfully.");
98 http_response_code(200);
99 return true;
100 }
101
102 self::log(" Webhook signature mismatch.", [], 'warning');
103 http_response_code(401);
104 return false;
105 }
106
107 protected static function handle_capture($payload) {
108 $transaction_id = sanitize_text_field($payload['id'] ?? '');
109 $order_id = absint($payload['merchantReferenceId'] ?? $payload['invoiceNumber'] ?? 0);
110 $capture_amount = floatval($payload['authAmount'] ?? 0);
111
112 if ($transaction_id && $order_id && $order = wc_get_order($order_id)) {
113 $current_status = $order->get_status();
114 $order_total = $order->get_total();
115
116 if ($capture_amount <= 0) {
117 $capture_amount = $order_total;
118 self::log("Amount not found in capture webhook, using order total", [
119 'order_total' => $order_total,
120 'used_amount' => $capture_amount
121 ], 'warning');
122 }
123
124 if (in_array($current_status, ['pending', 'on-hold', 'processing'], true)) {
125 if (abs($capture_amount - $order_total) < 0.01) {
126 $order->update_status('completed');
127 $note = sprintf(
128 __('Payment captured by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
129 esc_html($transaction_id),
130 wc_price($capture_amount)
131 );
132 self::log("Order #{$order_id} fully captured from webhook", [
133 'transaction_id' => $transaction_id,
134 'amount' => $capture_amount
135 ]);
136 } else {
137 $order->update_status('on-hold');
138 $note = sprintf(
139 __('Partial payment captured by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
140 esc_html($transaction_id),
141 wc_price($capture_amount)
142 );
143 self::log("Order #{$order_id} partially captured from webhook", [
144 'transaction_id' => $transaction_id,
145 'captured_amount' => $capture_amount,
146 'order_total' => $order_total
147 ]);
148 }
149
150 $order->add_order_note($note);
151 } else {
152 self::log("Capture webhook skipped for order #{$order_id} with status: {$current_status}", [
153 'transaction_id' => $transaction_id
154 ]);
155 }
156 }
157 }
158
159 protected static function handle_void($payload) {
160 $transaction_id = sanitize_text_field($payload['id'] ?? '');
161 $auth_code = sanitize_text_field($payload['authCode'] ?? '');
162 $order_id = absint($payload['merchantReferenceId'] ?? $payload['invoiceNumber'] ?? 0);
163 $void_amount = floatval($payload['authAmount'] ?? 0);
164
165 if ($transaction_id && $order_id && $order = wc_get_order($order_id)) {
166 $current_status = $order->get_status();
167 $order_total = $order->get_total();
168
169 if ($void_amount <= 0) {
170 $void_amount = $order_total;
171 self::log("Amount not found in void webhook, using order total", [
172 'order_total' => $order_total,
173 'used_amount' => $void_amount
174 ], 'warning');
175 }
176
177 if (!in_array($current_status, ['cancelled', 'refunded'], true)) {
178 if (abs($void_amount - $order_total) < 0.01) {
179 $order->update_status('cancelled');
180 $note = sprintf(
181 __('Payment voided by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
182 esc_html($transaction_id),
183 wc_price($void_amount)
184 );
185 self::log("Order #{$order_id} fully voided from webhook", [
186 'transaction_id' => $transaction_id,
187 'amount' => $void_amount
188 ]);
189 } else {
190 $order->update_status('on-hold');
191 $note = sprintf(
192 __('Partial payment voided by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
193 esc_html($transaction_id),
194 wc_price($void_amount)
195 );
196 self::log("Order #{$order_id} partially voided from webhook", [
197 'transaction_id' => $transaction_id,
198 'voided_amount' => $void_amount,
199 'order_total' => $order_total
200 ]);
201 }
202
203 $order->add_order_note($note);
204 } else {
205 self::log("Void webhook skipped for order #{$order_id} with status: {$current_status}", [
206 'transaction_id' => $transaction_id
207 ]);
208 }
209 }
210 }
211
212 protected static function handle_refund($payload) {
213 $transaction_id = sanitize_text_field($payload['id'] ?? '');
214 $auth_code = sanitize_text_field($payload['authCode'] ?? '');
215 $order_id = absint($payload['merchantReferenceId'] ?? $payload['invoiceNumber'] ?? 0);
216 $refund_amount = floatval($payload['authAmount'] ?? 0);
217
218 if ($transaction_id && $order_id && $order = wc_get_order($order_id)) {
219 $current_status = $order->get_status();
220 $order_total = $order->get_total();
221 $remaining_balance = $order_total - $order->get_total_refunded();
222
223 if ($refund_amount <= 0) {
224 $refund_amount = $remaining_balance;
225 self::log("Amount not found in refund webhook, using remaining balance", [
226 'order_total' => $order_total,
227 'already_refunded' => $order->get_total_refunded(),
228 'remaining_balance' => $remaining_balance,
229 'used_amount' => $refund_amount
230 ], 'warning');
231 }
232
233 if ($current_status !== 'refunded') {
234 if (abs($refund_amount - $remaining_balance) < 0.01) {
235 $order->update_status('refunded');
236 $note = sprintf(
237 __('Order refunded by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
238 esc_html($transaction_id),
239 wc_price($refund_amount)
240 );
241 self::log("Order #{$order_id} fully refunded from webhook", [
242 'transaction_id' => $transaction_id,
243 'amount' => $refund_amount
244 ]);
245 } else {
246 $note = sprintf(
247 __('Partial refund processed by Authorize.Net. Transaction ID: %1$s. Amount: %2$s', 'payment-gateway-for-authorize-net-for-woocommerce'),
248 esc_html($transaction_id),
249 wc_price($refund_amount)
250 );
251 self::log("Order #{$order_id} partially refunded from webhook", [
252 'transaction_id' => $transaction_id,
253 'refunded_amount' => $refund_amount,
254 'order_total' => $order_total,
255 'remaining_balance' => $remaining_balance
256 ]);
257 }
258
259 $order->add_order_note($note);
260 } else {
261 self::log("Refund webhook skipped for order #{$order_id} (already refunded)", [
262 'transaction_id' => $transaction_id
263 ]);
264 }
265 }
266 }
267
268 protected static function log($message, $context = [], $level = 'info') {
269 if (!class_exists('WC_Logger')) {
270 return;
271 }
272
273 if (!in_array($level, ['warning', 'error'], true) && self::$debug_mode !== 'yes') {
274 return;
275 }
276
277 $logger = wc_get_logger();
278 $log_context = ['source' => 'easyauthnet_authorizenet'];
279 $formatted_context = !empty($context) ? wp_json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '';
280 $full_message = $message . ($formatted_context ? "\n" . $formatted_context : '');
281
282 if (method_exists($logger, $level)) {
283 $logger->{$level}($full_message, $log_context);
284 } else {
285 $logger->info($full_message, $log_context);
286 }
287 }
288 }
289