PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.3
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.3
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / modules / webhook / classes / class-cartflows-webhook-dispatcher.php

class-cartflows-webhook-dispatcher.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.1.3, at modules/webhook/classes/class-cartflows-webhook-dispatcher.php

336 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Webhook Dispatcher.
4 *
5 * Core delivery engine. Schedules async webhook delivery via Action Scheduler.
6 *
7 * @package CartFlows
8 */
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 if ( ! class_exists( 'Cartflows_Webhook_Dispatcher' ) ) {
16
17 /**
18 * Class Cartflows_Webhook_Dispatcher.
19 */
20 class Cartflows_Webhook_Dispatcher {
21
22 /**
23 * Action Scheduler hook name.
24 *
25 * @var string
26 */
27 const AS_HOOK = 'cartflows_process_webhook_delivery';
28
29 /**
30 * Retry backoff intervals in seconds.
31 *
32 * @var array
33 */
34 const RETRY_INTERVALS = array( 60, 300, 1800, 3600, 7200 );
35
36 /**
37 * Member Variable
38 *
39 * @var instance
40 */
41 private static $instance = null;
42
43 /**
44 * Initiator
45 *
46 * @return object initialized object of class.
47 */
48 public static function get_instance() {
49 if ( is_null( self::$instance ) ) {
50 self::$instance = new self();
51 }
52 return self::$instance;
53 }
54
55 /**
56 * Constructor
57 */
58 public function __construct() {
59 add_action( self::AS_HOOK, array( $this, 'process_webhook_delivery' ), 10, 4 );
60 }
61
62 /**
63 * Dispatch webhooks for an event.
64 *
65 * Finds all enabled webhooks for the event and schedules Action Scheduler jobs.
66 *
67 * @param string $event Event name.
68 * @param array $context Event context data.
69 * @return void
70 */
71 public function dispatch( $event, $context = array() ) {
72 $manager = Cartflows_Webhook_Manager::get_instance();
73 $webhooks = $manager->get_webhooks_for_event( $event );
74
75 if ( empty( $webhooks ) ) {
76 wcf()->logger->log( 'Webhook dispatch: No webhooks found for event ' . $event );
77 return;
78 }
79
80 $object_id = isset( $context['order_id'] ) ? $context['order_id'] : ( isset( $context['step_id'] ) ? $context['step_id'] : 0 );
81
82 foreach ( $webhooks as $webhook ) {
83 // Deduplication check.
84 $dedup_key = 'wcf_wh_' . md5( $webhook['id'] . '_' . $event . '_' . $object_id );
85
86 if ( get_transient( $dedup_key ) ) {
87 wcf()->logger->log( 'Webhook dispatch: Skipping duplicate for webhook ' . $webhook['id'] . ' event ' . $event );
88 continue;
89 }
90
91 // Set dedup transient for 60 seconds.
92 set_transient( $dedup_key, 1, 60 );
93
94 $payload_builder = Cartflows_Webhook_Payload::get_instance();
95 $payload = $payload_builder->build( $event, $context, $webhook['id'] );
96 $body = wp_json_encode( $payload );
97
98 wcf()->logger->log( 'Webhook dispatch: Scheduling delivery for webhook ' . $webhook['id'] . ' event ' . $event );
99
100 // Schedule async delivery via Action Scheduler.
101 if ( function_exists( 'as_schedule_single_action' ) ) {
102 as_schedule_single_action(
103 time(),
104 self::AS_HOOK,
105 array(
106 'webhook_id' => $webhook['id'],
107 'body' => $body,
108 'event' => $event,
109 'retry_attempt' => 0,
110 ),
111 'cartflows-webhooks'
112 );
113 }
114 }
115 }
116
117 /**
118 * Process a webhook delivery (called by Action Scheduler).
119 *
120 * @param string $webhook_id Webhook ID.
121 * @param string $body JSON payload body.
122 * @param string $event Event name.
123 * @param int $retry_attempt Current retry attempt.
124 * @return void
125 */
126 public function process_webhook_delivery( $webhook_id, $body, $event, $retry_attempt = 0 ) {
127 $manager = Cartflows_Webhook_Manager::get_instance();
128 $webhook = $manager->get_webhook( $webhook_id );
129
130 if ( ! $webhook ) {
131 wcf()->logger->log( 'Webhook delivery: Webhook not found - ' . $webhook_id );
132 return;
133 }
134
135 if ( empty( $webhook['enabled'] ) ) {
136 wcf()->logger->log( 'Webhook delivery: Webhook disabled, skipping - ' . $webhook_id );
137 return;
138 }
139
140 $url = $webhook['url'];
141 $secret = isset( $webhook['secret'] ) ? $webhook['secret'] : '';
142
143 // Generate HMAC signature.
144 $signature = hash_hmac( 'sha256', $body, $secret );
145
146 $headers = array(
147 'Content-Type' => 'application/json',
148 'X-CartFlows-Webhook-Signature' => $signature,
149 'X-CartFlows-Event' => $event,
150 'X-CartFlows-Delivery-ID' => wp_generate_uuid4(),
151 'User-Agent' => 'CartFlows-Webhook/1.0',
152 );
153
154 wcf()->logger->log( 'Webhook delivery: Sending to ' . $url . ' for event ' . $event . ' (attempt ' . ( $retry_attempt + 1 ) . ')' );
155
156 $response = wp_safe_remote_post(
157 $url,
158 array(
159 'body' => $body,
160 'headers' => $headers,
161 'timeout' => 3,
162 'sslverify' => true,
163 )
164 );
165
166 if ( is_wp_error( $response ) ) {
167 $error_message = $response->get_error_message();
168 wcf()->logger->log( 'Webhook delivery failed: ' . $error_message . ' - Webhook: ' . $webhook_id );
169
170 $manager->update_last_delivery(
171 $webhook_id,
172 array(
173 'status' => 'failed',
174 'triggered_at' => current_time( 'mysql' ),
175 'error' => $error_message,
176 )
177 );
178
179 $this->maybe_schedule_retry( $webhook_id, $body, $event, $retry_attempt, $webhook );
180 return;
181 }
182
183 $response_code = wp_remote_retrieve_response_code( $response );
184
185 if ( $response_code >= 200 && $response_code < 300 ) {
186 wcf()->logger->log( 'Webhook delivery success: HTTP ' . $response_code . ' - Webhook: ' . $webhook_id );
187
188 $manager->update_last_delivery(
189 $webhook_id,
190 array(
191 'status' => 'success',
192 'triggered_at' => current_time( 'mysql' ),
193 'error' => '',
194 )
195 );
196 } else {
197 $error_message = 'HTTP ' . $response_code . ': ' . wp_remote_retrieve_response_message( $response );
198 wcf()->logger->log( 'Webhook delivery failed: ' . $error_message . ' - Webhook: ' . $webhook_id );
199
200 $manager->update_last_delivery(
201 $webhook_id,
202 array(
203 'status' => 'failed',
204 'triggered_at' => current_time( 'mysql' ),
205 'error' => $error_message,
206 )
207 );
208
209 $this->maybe_schedule_retry( $webhook_id, $body, $event, $retry_attempt, $webhook );
210 }
211 }
212
213 /**
214 * Schedule a retry if within max retries.
215 *
216 * @param string $webhook_id Webhook ID.
217 * @param string $body JSON payload body.
218 * @param string $event Event name.
219 * @param int $retry_attempt Current retry attempt.
220 * @param array $webhook Webhook configuration.
221 * @return void
222 */
223 private function maybe_schedule_retry( $webhook_id, $body, $event, $retry_attempt, $webhook ) {
224 $max_retries = isset( $webhook['max_retries'] ) ? absint( $webhook['max_retries'] ) : 3;
225 $next_attempt = $retry_attempt + 1;
226
227 if ( $next_attempt >= $max_retries ) {
228 wcf()->logger->log( 'Webhook delivery: Max retries reached (' . $max_retries . ') for webhook ' . $webhook_id . '. Giving up.' );
229 return;
230 }
231
232 $delay = isset( self::RETRY_INTERVALS[ $retry_attempt ] ) ? self::RETRY_INTERVALS[ $retry_attempt ] : 7200;
233
234 wcf()->logger->log( 'Webhook delivery: Scheduling retry ' . $next_attempt . ' in ' . $delay . 's for webhook ' . $webhook_id );
235
236 if ( function_exists( 'as_schedule_single_action' ) ) {
237 as_schedule_single_action(
238 time() + $delay,
239 self::AS_HOOK,
240 array(
241 'webhook_id' => $webhook_id,
242 'body' => $body,
243 'event' => $event,
244 'retry_attempt' => $next_attempt,
245 ),
246 'cartflows-webhooks'
247 );
248 }
249 }
250
251 /**
252 * Send a test webhook synchronously.
253 *
254 * @param string $webhook_id Webhook ID.
255 * @return array Result with 'success', 'response_code', and 'error' keys.
256 */
257 public function send_test( $webhook_id ) {
258 $manager = Cartflows_Webhook_Manager::get_instance();
259 $webhook = $manager->get_webhook( $webhook_id );
260
261 if ( ! $webhook ) {
262 return array(
263 'success' => false,
264 'response_code' => 0,
265 'error' => __( 'Webhook not found.', 'cartflows' ),
266 );
267 }
268
269 $payload_builder = Cartflows_Webhook_Payload::get_instance();
270 $payload = $payload_builder->build_test( $webhook_id );
271 $body = wp_json_encode( $payload );
272 $secret = isset( $webhook['secret'] ) ? $webhook['secret'] : '';
273 $signature = hash_hmac( 'sha256', $body, $secret );
274
275 $headers = array(
276 'Content-Type' => 'application/json',
277 'X-CartFlows-Webhook-Signature' => $signature,
278 'X-CartFlows-Event' => 'test',
279 'X-CartFlows-Delivery-ID' => wp_generate_uuid4(),
280 'User-Agent' => 'CartFlows-Webhook/1.0',
281 );
282
283 wcf()->logger->log( 'Webhook test: Sending test to ' . $webhook['url'] );
284
285 $response = wp_safe_remote_post(
286 $webhook['url'],
287 array(
288 'body' => $body,
289 'headers' => $headers,
290 'timeout' => 3,
291 'sslverify' => true,
292 )
293 );
294
295 if ( is_wp_error( $response ) ) {
296 $error_message = $response->get_error_message();
297 wcf()->logger->log( 'Webhook test failed: ' . $error_message );
298
299 $manager->update_last_delivery(
300 $webhook_id,
301 array(
302 'status' => 'failed',
303 'triggered_at' => current_time( 'mysql' ),
304 'error' => $error_message,
305 )
306 );
307
308 return array(
309 'success' => false,
310 'response_code' => 0,
311 'error' => $error_message,
312 );
313 }
314
315 $response_code = wp_remote_retrieve_response_code( $response );
316
317 $manager->update_last_delivery(
318 $webhook_id,
319 array(
320 'status' => ( $response_code >= 200 && $response_code < 300 ) ? 'success' : 'failed',
321 'triggered_at' => current_time( 'mysql' ),
322 'error' => ( $response_code >= 200 && $response_code < 300 ) ? '' : 'HTTP ' . $response_code,
323 )
324 );
325
326 wcf()->logger->log( 'Webhook test result: HTTP ' . $response_code );
327
328 return array(
329 'success' => ( $response_code >= 200 && $response_code < 300 ),
330 'response_code' => $response_code,
331 'error' => ( $response_code >= 200 && $response_code < 300 ) ? '' : 'HTTP ' . $response_code,
332 );
333 }
334 }
335 }
336