PluginProbe
WANotifier for Forms and Actions / 3.0.0
WANotifier for Forms and Actions v3.0.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.0.0, at includes/classes/class-notifier-connection.php

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