PluginProbe
WANotifier for Forms and Actions / 3.1.1
WANotifier for Forms and Actions v3.1.1
3.1.1 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 All 67 releases
notifier / includes / classes / class-notifier-connection.php

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

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