PluginProbe
WANotifier for Forms and Actions / 3.1.0
WANotifier for Forms and Actions v3.1.0
3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 1.0.3 All 66 releases
notifier / includes / classes / class-notifier-connection.php

class-notifier-connection.php in WANotifier for Forms and Actions 3.1.0, at includes/classes/class-notifier-connection.php

994 lines 37.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 /**
7 * Connection class for WANotifier plugin v3
8 *
9 * Manages per-integration connections between this WordPress site and the WANotifier SaaS.
10 * Each integration (woocommerce, gravityforms, cf7, etc.) has its own connection stored
11 * in WordPress options.
12 *
13 * Option keys used (per slug):
14 * notifier_conn_{slug}_auth_key
15 * notifier_conn_{slug}_webhook_key
16 * notifier_conn_{slug}_account_id
17 * notifier_conn_{slug}_connection_id
18 * notifier_conn_{slug}_saas_url
19 * notifier_conn_{slug}_connected_at
20 * notifier_conn_{slug}_token (temporary, cleared after connect)
21 * notifier_conn_{slug}_token_created (temporary)
22 *
23 * @package Notifier
24 */
25 class Notifier_Connection {
26
27 // ========================================
28 // 1. INITIALIZATION
29 // ========================================
30
31 public static function init() {
32 add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
33 add_action( 'notifier_fire_trigger', array( __CLASS__, 'handle_fire_trigger_action' ), 10, 3 );
34 }
35
36 // ========================================
37 // 2. CONNECTION MANAGEMENT
38 // ========================================
39
40 /**
41 * Generate (or retrieve existing) connect token for a slug.
42 *
43 * @param string $slug Integration slug.
44 * @return string Connect token.
45 */
46 public static function get_or_create_connect_token( $slug ) {
47 $slug = sanitize_key( $slug );
48 $token = get_option( 'notifier_conn_' . $slug . '_token' );
49
50 if ( ! $token ) {
51 $token = 'ct_' . wp_generate_password( 48, false );
52 update_option( 'notifier_conn_' . $slug . '_token', $token, false );
53 update_option( 'notifier_conn_' . $slug . '_token_created', time(), false );
54 }
55
56 return $token;
57 }
58
59 /**
60 * Complete the connection after SaaS calls back.
61 *
62 * @param string $slug Integration slug.
63 * @param array $data Connection data from SaaS (auth_key, webhook_key, account_id, etc.).
64 * @return bool
65 */
66 public static function complete_connection( $slug, $data ) {
67 $slug = sanitize_key( $slug );
68
69 $stored_token = get_option( 'notifier_conn_' . $slug . '_token' );
70 $connect_token = isset( $data['connect_token'] ) ? $data['connect_token'] : '';
71
72 if ( empty( $stored_token ) || ! hash_equals( $stored_token, $connect_token ) ) {
73 return false;
74 }
75
76 update_option( 'notifier_conn_' . $slug . '_auth_key', sanitize_text_field( $data['auth_key'] ?? '' ), false );
77 update_option( 'notifier_conn_' . $slug . '_webhook_key', sanitize_text_field( $data['webhook_key'] ?? '' ), false );
78 update_option( 'notifier_conn_' . $slug . '_account_id', sanitize_text_field( $data['account_id'] ?? '' ), false );
79 update_option( 'notifier_conn_' . $slug . '_connection_id', absint( $data['connection_id'] ?? 0 ), false );
80 update_option( 'notifier_conn_' . $slug . '_saas_url', esc_url_raw( $data['saas_url'] ?? ( NOTIFIER_APP_BASE_URL . '/' . NOTIFIER_APP_API_PATH . '/' ) ), false );
81 update_option( 'notifier_conn_' . $slug . '_connected_at', current_time( 'mysql' ), false );
82
83 // Clear the temporary token.
84 delete_option( 'notifier_conn_' . $slug . '_token' );
85 delete_option( 'notifier_conn_' . $slug . '_token_created' );
86
87 return true;
88 }
89
90 /**
91 * Check if an integration is connected.
92 *
93 * @param string $slug Integration slug.
94 * @return bool
95 */
96 public static function is_connected( $slug ) {
97 $slug = sanitize_key( $slug );
98 $auth_key = get_option( 'notifier_conn_' . $slug . '_auth_key', '' );
99 return ! empty( $auth_key );
100 }
101
102 /**
103 * Get full connection data for a slug, including extra stored options like continuous_sync.
104 *
105 * @param string $slug Integration slug.
106 * @return array|null Connection data or null if not connected.
107 */
108 public static function get_connection( $slug ) {
109 $slug = sanitize_key( $slug );
110 $info = self::get_connection_info( $slug );
111 if ( ! $info ) {
112 return null;
113 }
114 // Merge in any extra per-slug config stored as a single serialized option.
115 $extra = get_option( 'notifier_conn_' . $slug . '_extra', array() );
116 if ( is_array( $extra ) ) {
117 $info = array_merge( $info, $extra );
118 }
119 return $info;
120 }
121
122 /**
123 * Save extra connection config for a slug (e.g. continuous_sync).
124 *
125 * @param string $slug Integration slug.
126 * @param array $data Key-value pairs to store.
127 */
128 public static function save_connection( $slug, $data ) {
129 $slug = sanitize_key( $slug );
130 $current = get_option( 'notifier_conn_' . $slug . '_extra', array() );
131 if ( ! is_array( $current ) ) {
132 $current = array();
133 }
134 // Only persist the extra keys (not auth_key, webhook_key etc. which have own options).
135 $core_keys = array( 'auth_key', 'webhook_key', 'account_id', 'connection_id', 'saas_url', 'connected_at' );
136 foreach ( $data as $key => $value ) {
137 if ( ! in_array( $key, $core_keys, true ) ) {
138 $current[ $key ] = $value;
139 }
140 }
141 update_option( 'notifier_conn_' . $slug . '_extra', $current );
142 }
143
144 /**
145 * Get connection info for a slug.
146 *
147 * @param string $slug Integration slug.
148 * @return array|null Connection data or null if not connected.
149 */
150 public static function get_connection_info( $slug ) {
151 $slug = sanitize_key( $slug );
152
153 if ( ! self::is_connected( $slug ) ) {
154 return null;
155 }
156
157 return array(
158 'auth_key' => get_option( 'notifier_conn_' . $slug . '_auth_key', '' ),
159 'webhook_key' => get_option( 'notifier_conn_' . $slug . '_webhook_key', '' ),
160 'account_id' => get_option( 'notifier_conn_' . $slug . '_account_id', '' ),
161 'connection_id' => get_option( 'notifier_conn_' . $slug . '_connection_id', 0 ),
162 'saas_url' => get_option( 'notifier_conn_' . $slug . '_saas_url', NOTIFIER_APP_BASE_URL . '/' . NOTIFIER_APP_API_PATH . '/' ),
163 'connected_at' => get_option( 'notifier_conn_' . $slug . '_connected_at', '' ),
164 );
165 }
166
167 /**
168 * Disconnect an integration.
169 *
170 * @param string $slug Integration slug.
171 * @param bool $notify_saas Whether to notify the SaaS about the disconnect.
172 * @return bool
173 */
174 public static function disconnect( $slug, $notify_saas = true ) {
175 $slug = sanitize_key( $slug );
176 $conn = self::get_connection_info( $slug );
177
178 if ( $notify_saas && $conn ) {
179 $saas_url = trailingslashit( $conn['saas_url'] );
180 wp_remote_post(
181 $saas_url . NOTIFIER_APP_API_PATH . '/integrations/' . $slug . '/disconnect',
182 array(
183 'headers' => array(
184 'Content-Type' => 'application/json',
185 'X-Auth-Key' => $conn['auth_key'],
186 ),
187 'body' => '{}',
188 'timeout' => 10,
189 'sslverify' => true,
190 )
191 );
192 }
193
194 delete_option( 'notifier_conn_' . $slug . '_auth_key' );
195 delete_option( 'notifier_conn_' . $slug . '_webhook_key' );
196 delete_option( 'notifier_conn_' . $slug . '_account_id' );
197 delete_option( 'notifier_conn_' . $slug . '_connection_id' );
198 delete_option( 'notifier_conn_' . $slug . '_saas_url' );
199 delete_option( 'notifier_conn_' . $slug . '_connected_at' );
200 delete_option( 'notifier_conn_' . $slug . '_token' );
201 delete_option( 'notifier_conn_' . $slug . '_token_created' );
202
203 return true;
204 }
205
206 /**
207 * Build the SaaS connect URL for an integration.
208 *
209 * @param string $slug Integration slug.
210 * @return string URL to redirect user to for connection.
211 */
212 public static function get_connect_url( $slug ) {
213 $slug = sanitize_key( $slug );
214 $token = self::get_or_create_connect_token( $slug );
215
216 $saas_base = NOTIFIER_APP_BASE_URL;
217
218 $payload = base64_encode( wp_json_encode( array(
219 'store_url' => site_url(),
220 'connect_token' => $token,
221 'return_url' => admin_url( 'admin.php?page=notifier' ),
222 ) ) );
223
224 return $saas_base . '/integrations/' . $slug . '/connect?payload=' . rawurlencode( $payload );
225 }
226
227 // ========================================
228 // 3. TRIGGER DISPATCH (v3)
229 // ========================================
230
231 /**
232 * Dispatch a trigger event.
233 *
234 * Default: async via Action Scheduler — AS provides built-in retry (3 attempts with backoff).
235 * When the `notifier_disable_background_processing` option is `yes`, fires synchronously in the
236 * current request instead (no AS queueing, no retry, no dedupe).
237 *
238 * @param string $slug Integration slug.
239 * @param string $trigger_slug Trigger slug (e.g. 'woo_order_new').
240 * @param array $context_args Context args (object_type, object_id, or form entry data).
241 */
242 public static function dispatch_trigger( $slug, $trigger_slug, $context_args ) {
243 if ( ! self::is_connected( $slug ) ) {
244 return;
245 }
246
247 // Real-time mode: skip Action Scheduler and fire synchronously in the same request.
248 if ( 'yes' === get_option( 'notifier_disable_background_processing', 'no' ) ) {
249 self::handle_fire_trigger_action( $slug, $trigger_slug, $context_args );
250 return;
251 }
252
253 $object_id = isset( $context_args['object_id'] ) ? (int) $context_args['object_id'] : 0;
254 Notifier_Backend::insert_activity_log(
255 'debug',
256 sprintf( '[%s] Trigger scheduled: %s (object_id: %d)', $slug, $trigger_slug, $object_id )
257 );
258
259 $action_args = array(
260 'slug' => $slug,
261 'trigger_slug' => $trigger_slug,
262 'context_args' => $context_args,
263 );
264
265 // Prevent duplicate actions for the same trigger + context.
266 $existing = as_get_scheduled_actions( array(
267 'hook' => 'notifier_fire_trigger',
268 'args' => $action_args,
269 'group' => 'notifier',
270 'status' => ActionScheduler_Store::STATUS_PENDING,
271 ), 'ids' );
272
273 if ( ! empty( $existing ) ) {
274 Notifier_Backend::insert_activity_log(
275 'debug',
276 sprintf( '[%s] Duplicate trigger skipped: %s (object_id: %d)', $slug, $trigger_slug, $object_id )
277 );
278 return;
279 }
280
281 as_enqueue_async_action(
282 'notifier_fire_trigger',
283 $action_args,
284 'notifier'
285 );
286 }
287
288 /**
289 * Action Scheduler handler for notifier_fire_trigger.
290 * Builds the payload here (async) so the main request is not slowed down.
291 *
292 * @param string $slug
293 * @param string $trigger_slug
294 * @param array $context_args
295 */
296 public static function handle_fire_trigger_action( $slug, $trigger_slug, $context_args ) {
297 $object_type = isset( $context_args['object_type'] ) ? $context_args['object_type'] : '';
298 $object_id = isset( $context_args['object_id'] ) ? (int) $context_args['object_id'] : 0;
299
300 if ( 'user' === $object_type && $object_id ) {
301 $payload = Notifier_Notification_Merge_Tags::build_user_payload( $object_id );
302 } elseif ( 'comment' === $object_type && $object_id ) {
303 $payload = Notifier_Notification_Merge_Tags::build_comment_payload( $object_id );
304 } elseif ( $object_id && in_array( $object_type, array_merge( array( 'post', 'page', 'attachment' ), array_keys( get_post_types( array( 'public' => true ) ) ) ), true ) ) {
305 $payload = Notifier_Notification_Merge_Tags::build_post_payload( $object_id, $object_type );
306 } else {
307 $payload = self::build_trigger_payload( $trigger_slug, $context_args );
308 }
309
310 Notifier_Backend::insert_activity_log(
311 'debug',
312 sprintf(
313 '[%s] Trigger firing: %s (object_id: %d) | merge_tags: %s | recipient_fields: %s',
314 $slug,
315 $trigger_slug,
316 $object_id,
317 wp_json_encode( $payload['merge_tags_data'] ?? array() ),
318 wp_json_encode( $payload['recipient_fields'] ?? array() )
319 )
320 );
321
322 self::fire_trigger( $slug, $trigger_slug, $payload );
323 }
324
325 /**
326 * Fire a trigger by POSTing to the SaaS event endpoint.
327 *
328 * @param string $slug
329 * @param string $trigger_slug
330 * @param array $payload
331 * @return bool
332 */
333 public static function fire_trigger( $slug, $trigger_slug, $payload ) {
334 $conn = self::get_connection_info( $slug );
335
336 if ( ! $conn ) {
337 return false;
338 }
339
340 $saas_url = trailingslashit( $conn['saas_url'] );
341 $webhook_key = $conn['webhook_key'];
342 $auth_key = $conn['auth_key'];
343
344 $recipient_fields = isset( $payload['recipient_fields'] ) ? $payload['recipient_fields'] : array();
345 foreach ( $recipient_fields as $key => $value ) {
346 $recipient_fields[ $key ] = notifier_maybe_add_default_country_code( notifier_sanitize_phone_number( (string) $value ) );
347 }
348
349 $body = wp_json_encode( array(
350 'event_key' => $trigger_slug,
351 'merge_tags_data' => isset( $payload['merge_tags_data'] ) ? $payload['merge_tags_data'] : array(),
352 'recipient_fields' => $recipient_fields,
353 ) );
354
355 $response = wp_remote_post(
356 $saas_url . NOTIFIER_APP_API_PATH . '/integrations/' . $slug . '/' . $webhook_key . '/event',
357 array(
358 'headers' => array(
359 'Content-Type' => 'application/json',
360 'X-Auth-Key' => $auth_key,
361 ),
362 'body' => $body,
363 'timeout' => 30,
364 'sslverify' => true,
365 )
366 );
367
368 if ( is_wp_error( $response ) ) {
369 $error_msg = $response->get_error_message();
370 error_log( 'WANotifier: fire_trigger failed for ' . $slug . '/' . $trigger_slug . ': ' . $error_msg );
371 Notifier_Backend::insert_activity_log(
372 'error',
373 sprintf( '[%s] Trigger failed: %s — %s', $slug, $trigger_slug, $error_msg )
374 );
375 return false;
376 }
377
378 $code = wp_remote_retrieve_response_code( $response );
379 if ( $code >= 400 ) {
380 $body = wp_remote_retrieve_body( $response );
381 error_log( 'WANotifier: fire_trigger HTTP ' . $code . ' for ' . $slug . '/' . $trigger_slug . ': ' . $body );
382 Notifier_Backend::insert_activity_log(
383 'error',
384 sprintf( '[%s] Trigger error (HTTP %d): %s — %s', $slug, $code, $trigger_slug, $body )
385 );
386 return false;
387 }
388
389 Notifier_Backend::insert_activity_log(
390 'success',
391 sprintf( '[%s] Trigger sent successfully: %s', $slug, $trigger_slug )
392 );
393
394 return true;
395 }
396
397 // ========================================
398 // 4. SAAS API HELPERS
399 // ========================================
400
401 /**
402 * Send a request to the SaaS API for a specific integration.
403 *
404 * @param string $slug Integration slug.
405 * @param string $endpoint Endpoint path (relative to {NOTIFIER_APP_API_PATH}/integrations/{slug}/).
406 * @param array $body Request body.
407 * @param string $method HTTP method.
408 * @return array|false Decoded response body or false on failure.
409 */
410 public static function send_saas_request( $slug, $endpoint, $body = array(), $method = 'POST' ) {
411 $conn = self::get_connection_info( $slug );
412
413 if ( ! $conn ) {
414 return false;
415 }
416
417 $saas_url = trailingslashit( $conn['saas_url'] );
418 $url = $saas_url . NOTIFIER_APP_API_PATH . '/integrations/' . $slug . '/' . ltrim( $endpoint, '/' );
419
420 $response = wp_remote_request( $url, array(
421 'method' => $method,
422 'headers' => array(
423 'Content-Type' => 'application/json',
424 'X-Auth-Key' => $conn['auth_key'],
425 ),
426 'body' => wp_json_encode( $body ),
427 'timeout' => 30,
428 'sslverify' => true,
429 ) );
430
431 if ( is_wp_error( $response ) ) {
432 error_log( 'WANotifier: SaaS request failed for ' . $slug . '/' . $endpoint . ': ' . $response->get_error_message() );
433 return false;
434 }
435
436 $code = wp_remote_retrieve_response_code( $response );
437 if ( $code >= 400 ) {
438 error_log( 'WANotifier: SaaS request HTTP ' . $code . ' for ' . $slug . '/' . $endpoint );
439 return false;
440 }
441
442 return json_decode( wp_remote_retrieve_body( $response ), true );
443 }
444
445 // ========================================
446 // 5. REST API ENDPOINTS (plugin-side)
447 // ========================================
448
449 /**
450 * Register REST endpoints for each integration.
451 * These are called by the SaaS to complete the connection handshake.
452 */
453 public static function register_rest_endpoints() {
454 // GET /wp-json/notifier/v1/legacy-triggers — SaaS fetches legacy CPT trigger definitions during migration.
455 register_rest_route( 'notifier/v1', '/legacy-triggers', array(
456 'methods' => 'GET',
457 'callback' => array( __CLASS__, 'handle_rest_legacy_triggers' ),
458 'permission_callback' => array( __CLASS__, 'verify_legacy_api_key' ),
459 ) );
460
461 $slugs = self::get_supported_slugs();
462
463 foreach ( $slugs as $slug ) {
464 // POST /wp-json/notifier/v1/{slug}/connect — SaaS delivers credentials.
465 register_rest_route( 'notifier/v1', '/' . $slug . '/connect', array(
466 'methods' => 'POST',
467 'callback' => function( $request ) use ( $slug ) {
468 return Notifier_Connection::handle_rest_connect( $request, $slug );
469 },
470 'permission_callback' => '__return_true',
471 ) );
472
473 // POST /wp-json/notifier/v1/{slug}/disconnect — SaaS or admin disconnect.
474 register_rest_route( 'notifier/v1', '/' . $slug . '/disconnect', array(
475 'methods' => 'POST',
476 'callback' => function( $request ) use ( $slug ) {
477 return Notifier_Connection::handle_rest_disconnect( $request, $slug );
478 },
479 'permission_callback' => function( $request ) use ( $slug ) {
480 return Notifier_Connection::verify_auth_key( $request, $slug );
481 },
482 ) );
483
484 // GET /wp-json/notifier/v1/{slug}/status — SaaS health check.
485 register_rest_route( 'notifier/v1', '/' . $slug . '/status', array(
486 'methods' => 'GET',
487 'callback' => function( $request ) use ( $slug ) {
488 return Notifier_Connection::handle_rest_status( $request, $slug );
489 },
490 'permission_callback' => function( $request ) use ( $slug ) {
491 return Notifier_Connection::verify_auth_key( $request, $slug );
492 },
493 ) );
494
495 // POST /wp-json/notifier/v1/{slug}/sync — Sync button.
496 register_rest_route( 'notifier/v1', '/' . $slug . '/sync', array(
497 'methods' => 'POST',
498 'callback' => function( $request ) use ( $slug ) {
499 return Notifier_Connection::handle_rest_sync_triggers( $request, $slug );
500 },
501 'permission_callback' => function( $request ) use ( $slug ) {
502 return Notifier_Connection::verify_auth_key( $request, $slug );
503 },
504 ) );
505 }
506
507 }
508
509 /**
510 * Handle SaaS callback to complete connection.
511 *
512 * @param WP_REST_Request $request
513 * @param string $slug
514 * @return WP_REST_Response
515 */
516 public static function handle_rest_connect( $request, $slug ) {
517 $data = $request->get_json_params();
518
519 if ( empty( $data ) ) {
520 return new WP_REST_Response( array( 'error' => true, 'message' => 'No data received.' ), 400 );
521 }
522
523 $success = self::complete_connection( $slug, $data );
524
525 if ( ! $success ) {
526 return new WP_REST_Response( array( 'error' => true, 'message' => 'Invalid connect token.' ), 401 );
527 }
528
529 // Build and return the trigger schema so SaaS can initialize trigger defaults.
530 $trigger_schema = self::build_trigger_schema( $slug );
531
532 return new WP_REST_Response( array(
533 'error' => false,
534 'message' => 'Connected successfully.',
535 'triggers' => $trigger_schema,
536 ), 200 );
537 }
538
539 /**
540 * Handle disconnect request.
541 *
542 * @param WP_REST_Request $request
543 * @param string $slug
544 * @return WP_REST_Response
545 */
546 public static function handle_rest_disconnect( $request, $slug ) {
547 self::disconnect( $slug, false ); // Don't notify SaaS — it's calling us.
548
549 return new WP_REST_Response( array(
550 'error' => false,
551 'message' => 'Disconnected.',
552 ), 200 );
553 }
554
555 /**
556 * Handle status check.
557 *
558 * @param WP_REST_Request $request
559 * @param string $slug
560 * @return WP_REST_Response
561 */
562 public static function handle_rest_status( $request, $slug ) {
563 return new WP_REST_Response( array(
564 'error' => false,
565 'connected' => self::is_connected( $slug ),
566 'slug' => $slug,
567 'site_url' => site_url(),
568 ), 200 );
569 }
570
571 /**
572 * Handle sync request — push current schema to SaaS.
573 *
574 * @param WP_REST_Request $request
575 * @param string $slug
576 * @return WP_REST_Response
577 */
578 public static function handle_rest_sync_triggers( $request, $slug ) {
579 $trigger_schema = self::build_trigger_schema( $slug );
580
581 // Push to SaaS.
582 self::send_saas_request( $slug, 'sync', array( 'triggers' => $trigger_schema ) );
583
584 return new WP_REST_Response( array(
585 'error' => false,
586 'message' => 'Triggers synced.',
587 'triggers' => $trigger_schema,
588 ), 200 );
589 }
590
591 // ========================================
592 // 6. TRIGGER PAYLOAD BUILDER
593 // ========================================
594
595 /**
596 * Build the full trigger payload for v3 dispatch.
597 *
598 * Calls get_trigger_merge_tag_value() for ALL defined merge tags for the trigger
599 * (not just the user-selected subset from CPT). This is the key difference from v2.
600 *
601 * @param string $trigger Trigger ID (with or without site key prefix).
602 * @param array $context_args Context args (object_type, object_id, or form entry data).
603 * @return array Payload with merge_tags_data and recipient_fields.
604 */
605 public static function build_trigger_payload( $trigger, $context_args ) {
606 $all_triggers = Notifier_Notification_Triggers::get_notification_triggers();
607
608 // Find the trigger definition.
609 $trigger_def = null;
610 foreach ( $all_triggers as $group => $triggers ) {
611 foreach ( $triggers as $t ) {
612 if ( $t['id'] === $trigger || Notifier_Notification_Triggers::get_trigger_id_without_site_key( $t['id'] ) === Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger ) ) {
613 $trigger_def = $t;
614 break 2;
615 }
616 }
617 }
618
619 $merge_tags_data = array();
620 $recipient_fields = array();
621
622 if ( $trigger_def ) {
623 // Build merge tags — ALL defined tags, not just user-selected ones.
624 if ( ! empty( $trigger_def['merge_tags'] ) ) {
625 foreach ( $trigger_def['merge_tags'] as $group => $tags ) {
626 foreach ( $tags as $tag ) {
627 $merge_tags_data[ $tag['id'] ] = Notifier_Notification_Merge_Tags::get_trigger_merge_tag_value( $tag['id'], $context_args );
628 }
629 }
630 }
631
632 // Build recipient fields — ALL defined fields.
633 if ( ! empty( $trigger_def['recipient_fields'] ) ) {
634 foreach ( $trigger_def['recipient_fields'] as $group => $fields ) {
635 foreach ( $fields as $field ) {
636 $recipient_fields[ $field['id'] ] = Notifier_Notification_Merge_Tags::get_trigger_recipient_field_value( $field['id'], $context_args );
637 }
638 }
639 }
640 }
641
642 $payload = array(
643 'merge_tags_data' => $merge_tags_data,
644 'recipient_fields' => $recipient_fields,
645 );
646
647 /**
648 * Allow integrations to override or enrich the trigger payload.
649 *
650 * @param array $payload Payload with merge_tags_data and recipient_fields.
651 * @param string $trigger Trigger ID.
652 * @param array $context_args Context args.
653 */
654 return apply_filters( 'notifier_v3_trigger_payload', $payload, $trigger, $context_args );
655 }
656
657 // ========================================
658 // 7. TRIGGER SCHEMA BUILDER
659 // ========================================
660
661 /**
662 * Build the trigger schema for a given integration slug.
663 *
664 * Serialises existing trigger definitions (from get_notification_triggers() and
665 * integration-specific get_merge_tags()) into the format the SaaS expects.
666 * No new field definitions are written — existing definitions are reused.
667 *
668 * @param string $slug Integration slug.
669 * @return array Array of trigger schema objects.
670 */
671 public static function build_trigger_schema( $slug ) {
672 $schema = array();
673
674 switch ( $slug ) {
675 case 'wordpress':
676 $schema = self::build_wordpress_schema();
677 break;
678
679 case 'gravityforms':
680 case 'cf7':
681 case 'wpforms':
682 case 'ninjaforms':
683 case 'formidable':
684 case 'fluentforms':
685 case 'forminator':
686 case 'sureforms':
687 case 'wsform':
688 $schema = self::build_form_schema( $slug );
689 break;
690 }
691
692 return $schema;
693 }
694
695 /**
696 * Build WordPress core trigger schema.
697 *
698 * @return array
699 */
700 private static function build_wordpress_schema() {
701 $all_triggers = Notifier_Notification_Triggers::get_notification_triggers();
702 $wp_triggers = isset( $all_triggers['WordPress'] ) ? $all_triggers['WordPress'] : array();
703
704 $enabled_merge_tags = get_option( 'notifier_wp_enabled_merge_tags', array() );
705 $enabled_recipient_fields = get_option( 'notifier_wp_enabled_recipient_fields', array() );
706 $enabled_merge_tags = is_array( $enabled_merge_tags ) ? $enabled_merge_tags : array();
707 $enabled_recipient_fields = is_array( $enabled_recipient_fields ) ? $enabled_recipient_fields : array();
708
709 $schema = array();
710 foreach ( $wp_triggers as $trigger ) {
711 $trigger_id = $trigger['id'];
712 $base_slug = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
713
714 // Filter merge_tags: keep all predefined fields, only include enabled custom meta.
715 $filtered_merge_tags = array();
716 foreach ( ( $trigger['merge_tags'] ?? array() ) as $group => $tags ) {
717 $filtered = array();
718 foreach ( $tags as $tag ) {
719 if ( false !== strpos( $tag['id'], '_meta_' ) ) {
720 if ( ! in_array( $tag['id'], $enabled_merge_tags, true ) ) {
721 continue;
722 }
723 }
724 $filtered[] = $tag;
725 }
726 if ( ! empty( $filtered ) ) {
727 $filtered_merge_tags[ $group ] = $filtered;
728 }
729 }
730
731 // Filter recipient_fields: only include enabled custom meta.
732 $filtered_recipient_fields = array();
733 foreach ( ( $trigger['recipient_fields'] ?? array() ) as $group => $fields ) {
734 $filtered = array();
735 foreach ( $fields as $field ) {
736 if ( false !== strpos( $field['id'], '_meta_' ) ) {
737 if ( ! in_array( $field['id'], $enabled_recipient_fields, true ) ) {
738 continue;
739 }
740 }
741 $filtered[] = $field;
742 }
743 if ( ! empty( $filtered ) ) {
744 $filtered_recipient_fields[ $group ] = $filtered;
745 }
746 }
747
748 $schema[] = array(
749 'slug' => $base_slug,
750 'label' => $trigger['label'],
751 'description' => isset( $trigger['description'] ) ? $trigger['description'] : '',
752 'category' => 'automation',
753 'event_key' => $base_slug,
754 'merge_tags' => self::flatten_merge_tags( $filtered_merge_tags ),
755 'recipient_fields' => self::flatten_recipient_fields( $filtered_recipient_fields ),
756 );
757 }
758
759 return $schema;
760 }
761
762 /**
763 * Build form plugin trigger schema (per-form triggers).
764 *
765 * @param string $slug Integration slug.
766 * @return array
767 */
768 private static function build_form_schema( $slug ) {
769 $all_triggers = Notifier_Notification_Triggers::get_notification_triggers();
770
771 $group_map = array(
772 'gravityforms' => 'Gravity Forms',
773 'cf7' => 'Contact Form 7',
774 'wpforms' => 'WPForms',
775 'ninjaforms' => 'Ninja Forms',
776 'formidable' => 'Formidable Forms',
777 'fluentforms' => 'Fluent Forms',
778 'forminator' => 'Forminator Forms',
779 'sureforms' => 'SureForms',
780 'wsform' => 'WS Form',
781 );
782
783 $group_key = isset( $group_map[ $slug ] ) ? $group_map[ $slug ] : '';
784 $triggers = ! empty( $group_key ) && isset( $all_triggers[ $group_key ] ) ? $all_triggers[ $group_key ] : array();
785
786 $schema = array();
787 foreach ( $triggers as $trigger ) {
788 $trigger_id = $trigger['id'];
789 $base_slug = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
790
791 // Extract just the form name from the label (e.g. 'Form "Contact Us" is submitted' → 'Contact Us').
792 // The SaaS builds the full display label dynamically from form_name.
793 $schema_entry = array(
794 'slug' => $base_slug,
795 'label' => $trigger['label'],
796 'description' => isset( $trigger['description'] ) ? $trigger['description'] : '',
797 'category' => 'automation',
798 'event_key' => $base_slug,
799 'merge_tags' => self::flatten_merge_tags( $trigger['merge_tags'] ?? array() ),
800 'recipient_fields' => self::flatten_recipient_fields( $trigger['recipient_fields'] ?? array() ),
801 );
802 $form_name = self::extract_form_name( $trigger['label'] );
803 if ( ! empty( $form_name ) ) {
804 $schema_entry['form_name'] = $form_name;
805 }
806
807 $schema[] = $schema_entry;
808 }
809
810 return $schema;
811 }
812
813 /**
814 * Flatten grouped merge tags array to a flat list for SaaS.
815 *
816 * @param array $grouped_tags Grouped merge tags (group => [tag, ...]).
817 * @return array Flat list of {id, label, return_type}.
818 */
819 /**
820 * Extract just the form name from a trigger label like 'Form "Contact Us" is submitted'.
821 * Returns empty string for non-form triggers.
822 *
823 * @param string $label
824 * @return string
825 */
826 private static function extract_form_name( $label ) {
827 if ( preg_match( '/^Form ["\'](.+)["\'] is submitted$/i', $label, $m ) ) {
828 return $m[1];
829 }
830 return '';
831 }
832
833 private static function flatten_merge_tags( $grouped_tags ) {
834 $flat = array();
835 foreach ( $grouped_tags as $group => $tags ) {
836 foreach ( $tags as $tag ) {
837 $flat[] = array(
838 'id' => $tag['id'],
839 'label' => $tag['label'],
840 'return_type' => isset( $tag['return_type'] ) ? $tag['return_type'] : 'text',
841 'group' => $group,
842 );
843 }
844 }
845 return $flat;
846 }
847
848 /**
849 * Flatten grouped recipient fields to a flat list for SaaS.
850 *
851 * @param array $grouped_fields Grouped recipient fields.
852 * @return array Flat list of {id, label}.
853 */
854 private static function flatten_recipient_fields( $grouped_fields ) {
855 $flat = array();
856 foreach ( $grouped_fields as $group => $fields ) {
857 foreach ( $fields as $field ) {
858 $flat[] = array(
859 'id' => $field['id'],
860 'label' => $field['label'],
861 'group' => $group,
862 );
863 }
864 }
865 return $flat;
866 }
867
868 // ========================================
869 // 7. AUTHENTICATION HELPERS
870 // ========================================
871
872 /**
873 * Verify auth key from request header.
874 *
875 * @param WP_REST_Request $request
876 * @param string $slug
877 * @return bool
878 */
879 public static function verify_auth_key( $request, $slug ) {
880 $provided_key = $request->get_header( 'X-Auth-Key' );
881 if ( empty( $provided_key ) ) {
882 return false;
883 }
884
885 $stored_key = get_option( 'notifier_conn_' . sanitize_key( $slug ) . '_auth_key', '' );
886 if ( empty( $stored_key ) ) {
887 return false;
888 }
889
890 return hash_equals( $stored_key, $provided_key );
891 }
892
893 /**
894 * Verify the legacy API key for the /legacy-triggers endpoint.
895 *
896 * Accepts the old notifier_api_key via X-Auth-Key header or ?key= query param.
897 *
898 * @param WP_REST_Request $request
899 * @return bool
900 */
901 public static function verify_legacy_api_key( $request ) {
902 $provided_key = $request->get_header( 'X-Auth-Key' );
903 if ( empty( $provided_key ) ) {
904 $provided_key = $request->get_param( 'key' );
905 }
906
907 if ( empty( $provided_key ) ) {
908 return false;
909 }
910
911 $stored_key = trim( get_option( 'notifier_api_key', '' ) );
912 if ( empty( $stored_key ) ) {
913 return false;
914 }
915
916 return hash_equals( $stored_key, $provided_key );
917 }
918
919 /**
920 * Handle GET /wp-json/notifier/v1/legacy-triggers
921 *
922 * Returns legacy CPT trigger definitions so the SaaS can run trigger migration
923 * during the batch connect flow. Optionally filtered by ?slug=.
924 *
925 * @param WP_REST_Request $request
926 * @return WP_REST_Response
927 */
928 public static function handle_rest_legacy_triggers( $request ) {
929 $filter_slug = $request->get_param( 'slug' );
930 if ( $filter_slug ) {
931 $filter_slug = sanitize_key( $filter_slug );
932 }
933
934 $posts = get_posts( array(
935 'post_type' => 'wa_notifier_trigger',
936 'post_status' => 'publish',
937 'numberposts' => -1,
938 ) );
939
940 $triggers = array();
941
942 foreach ( $posts as $post ) {
943 $trigger_id = get_post_meta( $post->ID, NOTIFIER_PREFIX . 'trigger', true );
944 $base_trigger = Notifier_Notification_Triggers::get_trigger_id_without_site_key( $trigger_id );
945 $slug = Notifier_Notification_Triggers::get_integration_slug_for_trigger( $base_trigger );
946
947 if ( ! $slug ) {
948 continue;
949 }
950
951 if ( $filter_slug && $slug !== $filter_slug ) {
952 continue;
953 }
954
955 $triggers[] = array(
956 'post_id' => $post->ID,
957 'old_trigger_id' => $trigger_id,
958 'new_trigger_slug' => $base_trigger,
959 'slug' => $slug,
960 );
961 }
962
963 return new WP_REST_Response( array(
964 'error' => false,
965 'triggers' => $triggers,
966 ), 200 );
967 }
968
969 // ========================================
970 // 9. SUPPORTED SLUGS
971 // ========================================
972
973 /**
974 * Get list of supported integration slugs.
975 *
976 * @return string[]
977 */
978 public static function get_supported_slugs() {
979 return array(
980 'wordpress',
981 'gravityforms',
982 'cf7',
983 'wpforms',
984 'ninjaforms',
985 'formidable',
986 'fluentforms',
987 'forminator',
988 'sureforms',
989 'wsform',
990 );
991 }
992
993 }
994