PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
timetics / vendor / themewinter / email-notification-sdk / src / Flow / FlowManager.php

FlowManager.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.64, at vendor/themewinter/email-notification-sdk/src/Flow/FlowManager.php

609 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Ens\Flow;
3
4 use DateTime;
5 use Ens\Config;
6 use Ens\Email\EmailSender;
7 use Ens\Utils\Helpers;
8 use Ens\Whatsapp\WhatsappSender;
9
10 /**
11 * Class FlowManager
12 *
13 * @package Ens\Flow
14 *
15 * @since 1.0.0
16 */
17 class FlowManager {
18
19 protected $identifier;
20
21 /**
22 * FlowManager constructor.
23 *
24 * @since 1.0.0
25 *
26 * @param string $identifier The identifier.
27 */
28 public function __construct($identifier) {
29 $this->identifier = $identifier;
30 }
31
32 /**
33 * Handle the action.
34 *
35 * @since 1.0.0
36 *
37 * @param string $action The name of the action.
38 * @param array $data The data associated with the action.
39 */
40 public function handle( $action, $data ) {
41 $prefix_for_cpt = $this->identifier;
42 $post_type = $prefix_for_cpt . '-flow';
43
44 $meta_key = '_' . $prefix_for_cpt . '_notification_flow_trigger';
45
46 $flows = get_posts( [
47 'post_type' => $post_type,
48 'meta_key' => $meta_key,
49 'meta_value' => $action,
50 'post_status' => 'publish',
51 ] );
52
53 if ( empty( $flows ) || !is_array( $flows ) ) {
54 return;
55 }
56
57 foreach ( $flows as $flow ) {
58 if ( !isset( $flow->ID ) ) {
59 continue;
60 }
61
62 $flow_config = get_post_meta( $flow->ID, '_' . $prefix_for_cpt . '_notification_flow_flow_config', true );
63
64 if ( empty( $flow_config ) || !is_array( $flow_config ) ) {
65 continue;
66 }
67
68 $this->execute_flow( $flow_config, $data, $action );
69 }
70 }
71
72 /**
73 * Resume flow callback.
74 *
75 * @since 1.0.0
76 *
77 * @param int $flow_id The ID of the flow.
78 * @param string $resume_time The resume time.
79 */
80 public function resume_flow_callback( $flow_id, $resume_time ) {
81 if ( !$flow_id || !$resume_time ) {
82 return;
83 }
84
85 $checkpoint = $this->get_user_flow_checkpoint( $resume_time, $flow_id );
86
87 if ( empty( $checkpoint ) || !is_array( $checkpoint ) ) {
88 return;
89 }
90
91 if ( !isset( $checkpoint['resume_after'] ) || time() < $checkpoint['resume_after'] ) {
92 return;
93 }
94
95 if ( !isset( $checkpoint['flow_snapshot'] ) || !isset( $checkpoint['hook_data'] ) ) {
96 return;
97 }
98
99 $flow = $checkpoint['flow_snapshot'];
100 $hook_data = $checkpoint['hook_data'];
101 $resume_node = $checkpoint['resume_node'] ?? null;
102 $action = $checkpoint['action'];
103
104 $this->execute_flow( $flow, $hook_data, $action, $resume_node );
105 }
106
107 /**
108 * Execute a flow.
109 *
110 * @since 1.0.0
111 *
112 * @param array $flow The flow data.
113 * @param array $hook_data The hook data.
114 * @param string|null $resume_from_id The node ID to resume from.
115 */
116 public function execute_flow( $flow, $hook_data,$action, $resume_from_id = null ) {
117 if ( !isset( $flow['nodes'] ) || !is_array( $flow['nodes'] ) ||
118 !isset( $flow['edges'] ) || !is_array( $flow['edges'] ) ) {
119 return;
120 }
121
122 $nodes = $flow['nodes'];
123 $edges = $flow['edges'];
124
125 // Build node map with validation
126 $node_map = [];
127 foreach ( $nodes as $node ) {
128 if ( !isset( $node['id'], $node['name'] ) ) {
129 continue;
130 }
131 $node_map[$node['id']] = $node;
132 }
133
134 $start_node_id = $resume_from_id ?? $this->find_trigger_node( $nodes );
135 if ( !$start_node_id || !isset( $node_map[$start_node_id] ) ) {
136 return;
137 }
138
139 $current_node_id = $start_node_id;
140
141 while ( $current_node_id && isset( $node_map[$current_node_id] ) ) {
142 $node = $node_map[$current_node_id];
143 switch ( $node['name'] ) {
144 case 'trigger':
145 $current_node_id = $this->get_next_node_id( $edges, $node['id'] );
146 break;
147
148 case 'condition':
149 if ( !isset( $node['data'] ) || !is_array( $node['data'] ) ) {
150 $current_node_id = null;
151 break;
152 }
153
154 $field = $node['data']['field'] ?? null;
155 $operator = $node['data']['operator'] ?? null;
156 $value = $node['data']['value'] ?? null;
157
158 $field_value = $hook_data[$field] ?? null;
159
160 if ( $field === null || $operator === null ) {
161 $current_node_id = null;
162 break;
163 }
164
165 if(is_numeric($value) && $value > 9999999999 && isset($hook_data[$field.'_timestamp'])){
166 $field_value = $hook_data[$field.'_timestamp'];
167
168 $timestamp = (int) ($value / 1000);
169
170 // Create DateTime object in WordPress timezone
171 $dt = (new DateTime("@$timestamp"))->setTimezone(wp_timezone());
172
173 // For comparison: get timestamp in WordPress timezone
174 $value = $dt->getTimestamp();
175 }
176 if(is_string($value)){
177 $field_value = strtolower(preg_replace('/[^\w]/', '', trim($field_value)));
178 $value = strtolower(preg_replace('/[^\w]/', '', trim($value)));
179 }
180
181 $is_true = $this->compare_values( $field_value, $operator, $value );
182 $lebel = $is_true ? 'True' : 'false';
183 $current_node_id = $this->get_next_node_id(
184 $edges,
185 $node['id'],
186 $lebel
187 );
188 break;
189
190 case 'delay':
191 if ( !isset( $node['data'] ) || !is_array( $node['data'] ) ) {
192 $current_node_id = null;
193 break;
194 }
195
196 $delayUnit = $node['data']['delayUnit'] ?? 'seconds';
197 $delay = isset( $node['data']['delay'] ) ? (int) $node['data']['delay'] : 60;
198
199 $delay_depeneds_on = $node['data']['delayCondition'] ?? null;
200 $delay_condition = 'after';
201 $dependent_key = $delay_depeneds_on;
202
203
204 if(str_contains( $delay_depeneds_on, 'before' )) {
205 $delay_condition = 'before';
206 $dependent_key = str_replace( 'before_', '', $delay_depeneds_on );
207 }
208 if(str_contains( $delay_depeneds_on, 'after' )) {
209 $delay_condition = 'after';
210 $dependent_key = str_replace( 'after_', '', $delay_depeneds_on );
211 }
212
213 if(isset( $hook_data[$dependent_key.'_timestamp'] )) {
214 $dependent_key = $dependent_key.'_timestamp';
215 }
216
217 $dependency_value = isset( $hook_data[$dependent_key] ) ? ( $hook_data[$dependent_key] ?? current_time( 'timestamp' ) ) : current_time( 'timestamp' );
218
219 $general_prefix = $this->identifier;
220
221 // Calculate seconds based on unit
222 $seconds = $delay;
223 switch ( $delayUnit ) {
224 case 'minutes':
225 $seconds *= 60;
226 break;
227 case 'hours':
228 $seconds *= 60 * 60;
229 break;
230 case 'days':
231 $seconds *= 60 * 60 * 24;
232 break;
233 }
234
235 // Calculate resume time
236 $resume_time = ( $delay_condition == 'before' ) ?
237 $dependency_value - $seconds :
238 $dependency_value + $seconds;
239
240 $next_node_id = $this->get_next_node_id( $edges, $node['id'] );
241 if ( !$next_node_id ) {
242 $current_node_id = null;
243 break;
244 }
245
246 $flow_id = uniqid( 'flow_', true );
247
248
249
250 $post_id = $hook_data['post_id'] ?? null;
251
252 if ( $post_id && isset( $hook_data['previous_'.$dependent_key] ) ) {
253 $previous_resume_time = $hook_data['previous_'.$dependent_key];
254 $previous_resume_time = ( $delay_condition == 'before' ) ? $previous_resume_time - $seconds : $previous_resume_time + $seconds;
255
256 if( $previous_resume_time != $resume_time ) {
257 $hook = $general_prefix . '_resume_flow_after_delay';
258 $previous_flow_id = get_post_meta( $post_id, 'ens_flow_id', true );
259
260 if(wp_next_scheduled( $hook, [ 'flow_id' => $previous_flow_id, 'resume_time' => $previous_resume_time ] )){
261 $key = sprintf( 'flow_checkpoint_%s_%s', $previous_flow_id, $previous_resume_time );
262 delete_transient( $key );
263 wp_clear_scheduled_hook( $hook, [ 'flow_id' => $previous_flow_id, 'resume_time' => $previous_resume_time ] );
264 }
265 }
266 }
267
268 if(isset( $post_id )) {
269 update_post_meta( $post_id, 'ens_flow_id', $flow_id );
270 }
271
272 // Save checkpoint
273 $this->save_user_flow_checkpoint( $resume_time, $flow_id, [
274 'resume_node' => $next_node_id,
275 'resume_after' => $resume_time,
276 'flow_snapshot' => $flow,
277 'hook_data' => $hook_data,
278 'action' => $action
279 ] );
280
281
282 // Schedule resume
283 wp_schedule_single_event(
284 $resume_time,
285 $general_prefix . '_resume_flow_after_delay',
286 [
287 'flow_id' => $flow_id,
288 'resume_time' => $resume_time,
289 ]
290 );
291
292 return; // Pause execution
293
294 case 'email':
295 if ( !isset( $node['data'] ) || !is_array( $node['data'] ) ) {
296 $current_node_id = null;
297 break;
298 }
299
300 $receiverType = $node['data']['receiverType'] ?? null;
301 if ( $receiverType && isset( $hook_data[$receiverType] ) ) {
302 $user_email = $hook_data[$receiverType];
303 $user_email = apply_filters( Helpers::get_hook_name( $this->identifier, 'notification_sdk_to_emails' ), $hook_data[$receiverType], $hook_data, $action );
304
305 if(is_array( $user_email )) {
306 foreach ( $user_email as $key=>$email ) {
307 $this->send_email_to_user( $receiverType, $email, $node['data'], $hook_data, $action,$key );
308 }
309 }
310 else{
311 $this->send_email_to_user( $receiverType, $user_email, $node['data'], $hook_data, $action );
312 }
313 }
314 $current_node_id = $this->get_next_node_id( $edges, $node['id'] );
315 break;
316
317 case 'whatsapp':
318 if ( !isset( $node['data'] ) || !is_array( $node['data'] ) ) {
319 $current_node_id = null;
320 break;
321 }
322
323 $receiverType = $node['data']['receiverType'] ?? null;
324 if ( $receiverType && isset( $hook_data[$receiverType] ) ) {
325 $user_number = $hook_data[$receiverType];
326 $user_number = apply_filters(
327 Helpers::get_hook_name( $this->identifier, 'notification_sdk_to_whatsapp_numbers' ),
328 $hook_data[$receiverType],
329 $hook_data,
330 $action
331 );
332
333 if ( is_array( $user_number ) ) {
334 foreach ( $user_number as $key => $number ) {
335 $this->send_whatsapp_to_user( $receiverType, $number, $node['data'], $hook_data, $action, $key );
336 }
337 }
338 else {
339 $this->send_whatsapp_to_user( $receiverType, $user_number, $node['data'], $hook_data, $action );
340 }
341 }
342 $current_node_id = $this->get_next_node_id( $edges, $node['id'] );
343 break;
344
345 case 'end':
346 return;
347
348 default:
349 $current_node_id = $this->get_next_node_id( $edges, $node['id'] );
350 break;
351 }
352 }
353 }
354
355 /**
356 * Compare values with type safety.
357 *
358 * @since 1.0.0
359 *
360 * @param mixed $left The left value.
361 * @param string $operator The operator.
362 * @param mixed $right The right value.
363 *
364 * @return bool
365 */
366 public function compare_values( $left, $operator, $right ) {
367 if ( $operator === null ) {
368 return false;
369 }
370 switch ( $operator ) {
371 case '=':
372 return $left == $right;
373 case 'not_equal':
374 return $left != $right;
375 case 'greater_than':
376 if ( !is_numeric( $left ) || !is_numeric( $right ) ) {
377 return false;
378 }
379
380 return $left > $right;
381 case 'less_than':
382 if ( !is_numeric( $left ) || !is_numeric( $right ) ) {
383 return false;
384 }
385
386 return $left < $right;
387 case 'greater_equal':
388 if ( !is_numeric( $left ) || !is_numeric( $right ) ) {
389 return false;
390 }
391
392 return $left >= $right;
393 case 'less_equal':
394 if ( !is_numeric( $left ) || !is_numeric( $right ) ) {
395 return false;
396 }
397
398 return $left <= $right;
399 default:return false;
400 }
401 }
402
403 /**
404 * Find trigger node in nodes array.
405 *
406 * @since 1.0.0
407 *
408 * @param array $nodes The nodes array.
409 *
410 * @return string|null
411 */
412 public function find_trigger_node( $nodes ) {
413 foreach ( $nodes as $node ) {
414 if ( isset( $node['name'] ) && $node['name'] === 'trigger' && isset( $node['id'] ) ) {
415 return $node['id'];
416 }
417 }
418 return null;
419 }
420
421 /**
422 * Get next node ID from edges.
423 *
424 * @since 1.0.0
425 *
426 * @param array $edges The edges array.
427 * @param string $from_id The source node ID.
428 * @param string|null $label The edge label (optional).
429 *
430 * @return string|null
431 */
432 public function get_next_node_id( $edges, $from_id, $label = null ) {
433 foreach ( $edges as $edge ) {
434 if ( !isset( $edge['source'], $edge['target'] ) ) {
435 continue;
436 }
437
438 if ( $edge['source'] === $from_id ) {
439 if ( $label === null || ( isset( $edge['label'] ) && $edge['label'] === $label ) ) {
440 return $edge['target'];
441 }
442 }
443 }
444 return null;
445 }
446
447 /**
448 * Save user flow checkpoint.
449 *
450 * @since 1.0.0
451 *
452 * @param int $resume_time The timestamp to resume at.
453 * @param string $flow_id The flow ID.
454 * @param array $data The checkpoint data.
455 */
456 public function save_user_flow_checkpoint( $resume_time, $flow_id, $data ) {
457 if ( empty( $data ) || !isset( $data['resume_after'] ) ) {
458 return;
459 }
460
461 // Set a minimum expiration of 1 hour and maximum of 30 days
462 $expiration = max( HOUR_IN_SECONDS, min( $data['resume_after'] - time() + DAY_IN_SECONDS, 30 * DAY_IN_SECONDS ) );
463 $key = sprintf( 'flow_checkpoint_%s_%s', $flow_id, $resume_time );
464
465 // Store the flow_id and resume_time in the data for verification
466 $data['_flow_id'] = $flow_id;
467 $data['_resume_time'] = $resume_time;
468
469 // Store the current timestamp to help with debugging
470 $data['_saved_at'] = time();
471
472 set_transient( $key, $data, $expiration );
473 }
474
475 /**
476 * Get user flow checkpoint.
477 *
478 * @since 1.0.0
479 *
480 * @param int $resume_time The resume time.
481 * @param string $flow_id The flow ID.
482 *
483 * @return array|null
484 */
485 public function get_user_flow_checkpoint( $resume_time, $flow_id ) {
486 $key = sprintf( 'flow_checkpoint_%s_%s', $flow_id, $resume_time );
487 $checkpoint = get_transient( $key );
488
489 if (!is_array($checkpoint)) {
490 // Log additional debug info
491 return null;
492 }
493
494 // Verify the flow_id and resume_time match what we expect
495 if (($checkpoint['_flow_id'] ?? '') !== $flow_id ||
496 ($checkpoint['_resume_time'] ?? 0) != $resume_time) {
497 return null;
498 }
499
500 return $checkpoint;
501 }
502
503 /**
504 * Send email to user with validation.
505 *
506 * @since 1.0.0
507 *
508 * @param string $receiverType The type of receiver.
509 * @param string $email The recipient email.
510 * @param array $data The email data.
511 * @param array $action_data The action data.
512 * @param string $action_name The action name.
513 * @param int $count The email count.
514 */
515 public function send_email_to_user( $receiverType, $email, $data, $action_data, $action_name, $count = 0 ) {
516 if ( !filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
517 return;
518 }
519
520 $from = $data['from'] ?? '';
521 $subject = $data['subject'] ?? '';
522 $body = $data['body'] ?? '';
523
524 if ( empty( $subject ) ) {
525 return;
526 }
527
528 $email_sender = new EmailSender( $this->identifier, $action_name, $receiverType, $email, $from, $subject, $body, $action_data, $count );
529 $email_sender->send();
530 }
531
532 /**
533 * Send WhatsApp message to user with loose validation.
534 *
535 * @since 1.0.0
536 *
537 * @param string $receiverType The type of receiver.
538 * @param mixed $number The recipient phone number.
539 * @param array $data The action node data (messageBody, etc.).
540 * @param array $action_data The hook payload.
541 * @param string $action_name The triggered action name.
542 * @param int $count Iteration count when receiver is an array.
543 */
544 public function send_whatsapp_to_user( $receiverType, $number, $data, $action_data, $action_name, $count = 0 ) {
545 if ( ! is_scalar( $number ) ) {
546 return;
547 }
548 $number = trim( (string) $number );
549 if ( '' === $number ) {
550 return;
551 }
552
553 // Require at least 7 digits (E.164 sanity).
554 if ( strlen( preg_replace( '/\D/', '', $number ) ) < 7 ) {
555 return;
556 }
557
558 $message_type = isset( $data['messageType'] ) && 'template' === $data['messageType'] ? 'template' : 'text';
559
560 if ( 'template' === $message_type ) {
561 $template_name = trim( (string) ( $data['templateName'] ?? '' ) );
562 if ( '' === $template_name ) {
563 return;
564 }
565
566 $body_params = $data['templateBodyParams'] ?? [];
567 if ( is_string( $body_params ) ) {
568 // TagInput normally returns an array, but accept newline-separated string for resilience.
569 $body_params = array_values( array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', $body_params ) ), 'strlen' ) );
570 }
571 if ( ! is_array( $body_params ) ) {
572 $body_params = [];
573 }
574
575 $sender = new WhatsappSender( [
576 'action_name' => $action_name,
577 'receiver_type' => $receiverType,
578 'to' => $number,
579 'message' => '',
580 'message_type' => 'template',
581 'template' => [
582 'name' => $template_name,
583 'language' => trim( (string) ( $data['templateLanguage'] ?? 'en_US' ) ) ?: 'en_US',
584 'body_params' => $body_params,
585 ],
586 'action_data' => $action_data,
587 'count' => $count,
588 ] );
589 $sender->send();
590 return;
591 }
592
593 $message = $data['messageBody'] ?? '';
594 if ( '' === trim( (string) $message ) ) {
595 return;
596 }
597
598 $sender = new WhatsappSender( [
599 'action_name' => $action_name,
600 'receiver_type' => $receiverType,
601 'to' => $number,
602 'message' => $message,
603 'message_type' => 'text',
604 'action_data' => $action_data,
605 'count' => $count,
606 ] );
607 $sender->send();
608 }
609 }