PluginProbe
WANotifier for Forms and Actions / 3.0.1
WANotifier for Forms and Actions v3.0.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 1.0.3 All 66 releases
notifier / includes / classes / class-notifier-connection.php

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

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