PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.7.7
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.7.7
8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 8.5.5 8.5.4 8.5.3 8.5.2 All 533 releases
chatbot / addons / automator / includes / core / plugin.php

plugin.php in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.7.7, at addons/automator/includes/core/plugin.php

1,383 lines 39.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main Plugin Core Class
4 *
5 * @package WPbot_Automator
6 */
7
8 namespace WPbot_Automator\Core;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Plugin class
16 */
17 class Plugin {
18
19 /**
20 * Initialize the plugin
21 */
22 public static function init() {
23 // Initialize Registry on init hook to avoid early translation loading.
24 add_action( 'init', array( Registry::class, 'init' ) );
25
26 // Initialize deactivation survey.
27 if ( is_admin() ) {
28 new Deactivation_Survey( WPBOT_AUTOMATOR_PLUGIN_DIR . 'wpbot-automator.php' );
29 }
30
31 // Initialize Tables module.
32 Tables::init();
33
34 // Initialize hooks.
35 add_action( 'admin_menu', array( __CLASS__, 'register_admin_menu' ) );
36 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_assets' ) );
37 add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
38
39 // Diagnostic hook to catch the source of "wpdb::prepare called incorrectly".
40 add_action( 'doing_it_wrong_run', function( $function, $message ) {
41 if ( 'wpdb::prepare' === $function || strpos( $message, 'wpdb::prepare' ) !== false ) {
42 // error_log( 'WPBOT-AUTOMATOR-DIAGNOSTIC: ' . $message );
43 // error_log( 'BACKTRACE: ' . wp_debug_backtrace_summary() );
44 }
45 }, 10, 2 );
46 }
47
48 /**
49 * Register admin menu
50 */
51 public static function register_admin_menu() {
52 // All menu registration is now handled by chatbot (qcld-wpwbot.php)
53 // We no longer register submenus here to keep the sidebar clean.
54 }
55
56 /**
57 * Render admin page
58 */
59 public static function render_admin_page() {
60 $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'workflows';
61
62 $tabs = array(
63 'workflows' => __( 'Workflows', 'wpbot-automator' ),
64 'email-templates' => __( 'Email Templates', 'wpbot-automator' ),
65 'logs' => __( 'Activity Log', 'wpbot-automator' ),
66 'tables' => __( 'Tables', 'wpbot-automator' ),
67 'support' => __( 'Support', 'wpbot-automator' ),
68 'help' => __( 'Help', 'wpbot-automator' ),
69 );
70
71 ?>
72 <div class="wrap wpbot-automator-wrap">
73 <h2><?php esc_html_e( 'Automator', 'wpbot-automator' ); ?></h2>
74 <h2 class="nav-tab-wrapper">
75 <?php foreach ( $tabs as $tab_id => $tab_name ) : ?>
76 <a href="?page=wpbot-automator&tab=<?php echo esc_attr( $tab_id ); ?>" class="nav-tab <?php echo $active_tab === $tab_id ? 'nav-tab-active' : ''; ?>">
77 <?php echo esc_html( $tab_name ); ?>
78 </a>
79 <?php endforeach; ?>
80 </h2>
81
82 <div class="wpbot-automator-tab-content" style="margin-top: 20px;">
83 <?php
84 switch ( $active_tab ) {
85 case 'email-templates':
86 self::render_email_templates_page();
87 break;
88 case 'logs':
89 self::render_logs_page();
90 break;
91 case 'tables':
92 self::render_tables_page();
93 break;
94 case 'support':
95 self::render_support_page();
96 break;
97 case 'help':
98 self::render_help_page();
99 break;
100 case 'workflows':
101 default:
102 echo '<div id="wpbot-automator-root"></div>';
103 break;
104 }
105 ?>
106 </div>
107 </div>
108 <?php
109 }
110
111 public static function render_support_page() {
112 include_once WPBOT_AUTOMATOR_PLUGIN_DIR . 'includes/templates/support-page.php';
113 }
114
115 public static function render_help_page() {
116 include_once WPBOT_AUTOMATOR_PLUGIN_DIR . 'includes/templates/help-page.php';
117 }
118
119 /**
120 * Render email templates page (Builder Root)
121 */
122 public static function render_email_templates_page() {
123 echo '<div id="wpbot-email-builder-root"></div>';
124 }
125
126 public static function render_logs_page() {
127 echo '<div id="wpbot-activity-log-root"></div>';
128 }
129
130 public static function render_tables_page() {
131 echo '<div id="wpbot-tables-root"></div>';
132 }
133 public static function wpbotauto_valid_license() {
134 $wpbot_automator_license_valid = get_option('wpbot_automator_license_valid');
135 if( $wpbot_automator_license_valid === 'automator_master_license' ) {
136 return true;
137 }
138 return false;
139 }
140 /**
141 * Enqueue admin assets
142 *
143 * @param string $hook Current admin page hook.
144 */
145 public static function enqueue_admin_assets( $hook ) {
146 // Only enqueue on our specific automator page
147 if ( ! isset( $_GET['page'] ) || 'wpbot-automator' !== $_GET['page'] ) {
148 return;
149 }
150
151 $asset_file = WPBOT_AUTOMATOR_PLUGIN_DIR . 'build/index.asset.php';
152
153 if ( file_exists( $asset_file ) ) {
154 $asset = require $asset_file;
155
156 wp_enqueue_script(
157 'wpbot-automator-admin',
158 WPBOT_AUTOMATOR_PLUGIN_URL . 'build/index.js',
159 $asset['dependencies'],
160 $asset['version'],
161 true
162 );
163
164 wp_enqueue_style(
165 'wpbot-automator-admin',
166 WPBOT_AUTOMATOR_PLUGIN_URL . 'build/index.css',
167 array( 'wp-components' ),
168 $asset['version']
169 );
170
171 $is_pro_active = self::wpbotauto_valid_license();
172
173 // Pass data to JavaScript.
174 wp_localize_script(
175 'wpbot-automator-admin',
176 'wpbotAutomator',
177 array(
178 'apiUrl' => rest_url( 'wpbot-automator/v1' ),
179 'nonce' => wp_create_nonce( 'wp_rest' ),
180 'pluginUrl' => WPBOT_AUTOMATOR_PLUGIN_URL,
181 'userEmail' => get_option( 'admin_email' ),
182 'isProActive' => $is_pro_active,
183 )
184 );
185 }
186 }
187
188 /**
189 * Register REST API routes
190 */
191 public static function register_rest_routes() {
192 // Workflows endpoint.
193 register_rest_route(
194 'wpbot-automator/v1',
195 '/workflows',
196 array(
197 'methods' => 'GET',
198 'callback' => array( __CLASS__, 'get_workflows' ),
199 'permission_callback' => array( __CLASS__, 'check_permission' ),
200 )
201 );
202
203 register_rest_route(
204 'wpbot-automator/v1',
205 '/workflows',
206 array(
207 'methods' => 'POST',
208 'callback' => array( __CLASS__, 'create_workflow' ),
209 'permission_callback' => array( __CLASS__, 'check_permission' ),
210 )
211 );
212
213 register_rest_route(
214 'wpbot-automator/v1',
215 '/workflows/(?P<id>\d+)',
216 array(
217 'methods' => 'PUT',
218 'callback' => array( __CLASS__, 'update_workflow' ),
219 'permission_callback' => array( __CLASS__, 'check_permission' ),
220 )
221 );
222
223 register_rest_route(
224 'wpbot-automator/v1',
225 '/workflows/(?P<id>\d+)',
226 array(
227 'methods' => 'DELETE',
228 'callback' => array( __CLASS__, 'delete_workflow' ),
229 'permission_callback' => array( __CLASS__, 'check_permission' ),
230 )
231 );
232
233 // Products endpoint for WooCommerce.
234 register_rest_route(
235 'wpbot-automator/v1',
236 '/products',
237 array(
238 'methods' => 'GET',
239 'callback' => array( __CLASS__, 'get_products' ),
240 'permission_callback' => array( __CLASS__, 'check_permission' ),
241 )
242 );
243
244 // WPForms forms list.
245 register_rest_route(
246 'wpbot-automator/v1',
247 '/wpforms-forms',
248 array(
249 'methods' => 'GET',
250 'callback' => array( __CLASS__, 'get_wpforms_forms' ),
251 'permission_callback' => array( __CLASS__, 'check_permission' ),
252 )
253 );
254
255 // Conversational forms list.
256 register_rest_route(
257 'wpbot-automator/v1',
258 '/conversational-forms',
259 array(
260 'methods' => 'GET',
261 'callback' => array( __CLASS__, 'get_conversational_forms' ),
262 'permission_callback' => array( __CLASS__, 'check_permission' ),
263 )
264 );
265
266 // Contact Form 7 forms list.
267 register_rest_route(
268 'wpbot-automator/v1',
269 '/cf7-forms',
270 array(
271 'methods' => 'GET',
272 'callback' => array( __CLASS__, 'get_cf7_forms' ),
273 'permission_callback' => array( __CLASS__, 'check_permission' ),
274 )
275 );
276
277 // Test action endpoint.
278 register_rest_route(
279 'wpbot-automator/v1',
280 '/test-action',
281 array(
282 'methods' => 'POST',
283 'callback' => array( __CLASS__, 'test_action' ),
284 'permission_callback' => array( __CLASS__, 'check_permission' ),
285 )
286 );
287
288 // Logs endpoint.
289 register_rest_route(
290 'wpbot-automator/v1',
291 '/logs',
292 array(
293 array(
294 'methods' => 'GET',
295 'callback' => array( __CLASS__, 'get_logs' ),
296 'permission_callback' => array( __CLASS__, 'check_permission' ),
297 ),
298 array(
299 'methods' => 'DELETE',
300 'callback' => array( __CLASS__, 'clear_logs' ),
301 'permission_callback' => array( __CLASS__, 'check_permission' ),
302 ),
303 )
304 );
305 register_rest_route(
306 'wpbot-automator/v1',
307 '/logs/summary',
308 array(
309 'methods' => 'GET',
310 'callback' => array( __CLASS__, 'get_logs_summary' ),
311 'permission_callback' => array( __CLASS__, 'check_permission' ),
312 )
313 );
314
315 // Export workflows endpoint.
316 register_rest_route(
317 'wpbot-automator/v1',
318 '/workflows/export',
319 array(
320 'methods' => 'GET',
321 'callback' => array( __CLASS__, 'export_workflows' ),
322 'permission_callback' => array( __CLASS__, 'check_permission' ),
323 )
324 );
325
326 // Import workflows endpoint.
327 register_rest_route(
328 'wpbot-automator/v1',
329 '/workflows/import',
330 array(
331 'methods' => 'POST',
332 'callback' => array( __CLASS__, 'import_workflows' ),
333 'permission_callback' => array( __CLASS__, 'check_permission' ),
334 )
335 );
336
337 // AI Workflow Generator endpoint.
338 register_rest_route(
339 'wpbot-automator/v1',
340 '/generate-workflow',
341 array(
342 'methods' => 'POST',
343 'callback' => array( __CLASS__, 'generate_workflow' ),
344 'permission_callback' => array( __CLASS__, 'check_permission' ),
345 )
346 );
347
348 // Support request endpoint.
349 register_rest_route(
350 'wpbot-automator/v1',
351 '/support/send',
352 array(
353 'methods' => 'POST',
354 'callback' => array( __CLASS__, 'send_support_email' ),
355 'permission_callback' => array( __CLASS__, 'check_permission' ),
356 )
357 );
358
359 // Credentials endpoint (GET / POST).
360 register_rest_route(
361 'wpbot-automator/v1',
362 '/credentials',
363 array(
364 array(
365 'methods' => 'GET',
366 'callback' => array( __CLASS__, 'get_credentials' ),
367 'permission_callback' => array( __CLASS__, 'check_permission' ),
368 ),
369 array(
370 'methods' => 'POST',
371 'callback' => array( __CLASS__, 'save_credential' ),
372 'permission_callback' => array( __CLASS__, 'check_permission' ),
373 )
374 )
375 );
376
377 // Credentials delete endpoint.
378 register_rest_route(
379 'wpbot-automator/v1',
380 '/credentials/(?P<id>[a-zA-Z0-9_\-]+)',
381 array(
382 'methods' => 'DELETE',
383 'callback' => array( __CLASS__, 'delete_credential' ),
384 'permission_callback' => array( __CLASS__, 'check_permission' ),
385 )
386 );
387
388 // Email Templates endpoint.
389 register_rest_route(
390 'wpbot-automator/v1',
391 '/email-templates',
392 array(
393 array(
394 'methods' => 'GET',
395 'callback' => array( __CLASS__, 'get_email_templates' ),
396 'permission_callback' => array( __CLASS__, 'check_permission' ),
397 ),
398 array(
399 'methods' => 'POST',
400 'callback' => array( __CLASS__, 'save_email_template' ),
401 'permission_callback' => array( __CLASS__, 'check_permission' ),
402 )
403 )
404 );
405
406 register_rest_route(
407 'wpbot-automator/v1',
408 '/email-templates/(?P<id>\d+)',
409 array(
410 array(
411 'methods' => 'GET',
412 'callback' => array( __CLASS__, 'get_email_template' ),
413 'permission_callback' => array( __CLASS__, 'check_permission' ),
414 ),
415 array(
416 'methods' => 'PUT',
417 'callback' => array( __CLASS__, 'save_email_template' ),
418 'permission_callback' => array( __CLASS__, 'check_permission' ),
419 ),
420 array(
421 'methods' => 'DELETE',
422 'callback' => array( __CLASS__, 'delete_email_template' ),
423 'permission_callback' => array( __CLASS__, 'check_permission' ),
424 )
425 )
426 );
427
428 // Webhook trigger endpoint — registered here (in addition to Webhook class)
429 // to guarantee the route is always available regardless of hook timing.
430 register_rest_route(
431 'wpbot-automator/v1',
432 '/webhook/(?P<token>[a-zA-Z0-9\-_]+)',
433 array(
434 'methods' => array( 'GET', 'POST' ),
435 'callback' => array( __CLASS__, 'handle_webhook_trigger' ),
436 'permission_callback' => '__return_true',
437 )
438 );
439 }
440
441 /**
442 * Handle incoming webhook — public entry point registered in register_rest_routes().
443 *
444 * Accepts any GET/POST request, extracts the token from the URL, merges all
445 * request params as trigger data, and fires the matching workflow.
446 *
447 * @param \WP_REST_Request $request Request object.
448 * @return \WP_REST_Response
449 */
450 public static function handle_webhook_trigger( \WP_REST_Request $request ) {
451 $token = $request->get_param( 'token' );
452 $data = $request->get_params();
453
454 // target_node_id tells the workflow runner which trigger node to start from.
455 $data['target_node_id'] = 'node-' . $token;
456
457 \WPbot_Automator\Engine\Workflow_Runner::run_workflows_for_trigger(
458 'webhook:incoming_webhook',
459 $data
460 );
461
462 return rest_ensure_response( array(
463 'success' => true,
464 'message' => 'Webhook received and workflow triggered.',
465 'token' => $token,
466 ) );
467 }
468
469 /**
470 * Get logs
471 *
472 * @param \WP_REST_Request $request Request object.
473 * @return \WP_REST_Response
474 */
475 public static function get_logs( $request ) {
476 global $wpdb;
477 $table_logs = Database::get_logs_table();
478 $table_workflows = Database::get_workflows_table();
479
480 $workflow_id = absint( $request->get_param( 'workflow_id' ) );
481 $status = sanitize_text_field( $request->get_param( 'status' ) );
482 $per_page = min( max( absint( $request->get_param( 'per_page' ) ?: 25 ), 1 ), 200 );
483 $page = max( absint( $request->get_param( 'page' ) ?: 1 ), 1 );
484 $offset = ( $page - 1 ) * $per_page;
485
486 $where_parts = array();
487 $query_params = array();
488
489 if ( $workflow_id ) {
490 $where_parts[] = 'l.workflow_id = %d';
491 $query_params[] = $workflow_id;
492 }
493 if ( $status && in_array( $status, array( 'success', 'failed' ), true ) ) {
494 $where_parts[] = 'l.status = %s';
495 $query_params[] = $status;
496 }
497
498 $where = $where_parts ? 'WHERE ' . implode( ' AND ', $where_parts ) : '';
499
500 $count_sql = "SELECT COUNT(*) FROM {$table_logs} l {$where}";
501 $total = (int) ( $query_params
502 ? $wpdb->get_var( $wpdb->prepare( $count_sql, ...$query_params ) )
503 : $wpdb->get_var( $count_sql ) );
504
505 $list_params = array_merge( $query_params, array( $per_page, $offset ) );
506 $sql = "SELECT l.*, w.name as workflow_name
507 FROM {$table_logs} l
508 LEFT JOIN {$table_workflows} w ON l.workflow_id = w.id
509 {$where}
510 ORDER BY l.created_at DESC
511 LIMIT %d OFFSET %d";
512
513 $logs = $wpdb->get_results( $wpdb->prepare( $sql, ...$list_params ), ARRAY_A );
514
515 return rest_ensure_response( array(
516 'logs' => $logs ?: array(),
517 'total' => $total,
518 'page' => $page,
519 'per_page' => $per_page,
520 'total_pages' => $total > 0 ? (int) ceil( $total / $per_page ) : 0,
521 ) );
522 }
523
524 public static function get_logs_summary() {
525 global $wpdb;
526 $table = Database::get_logs_table();
527 $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
528 $success = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'success'" );
529 $failed = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE status = 'failed'" );
530 $last = $wpdb->get_var( "SELECT created_at FROM {$table} ORDER BY created_at DESC LIMIT 1" );
531
532 return rest_ensure_response( array(
533 'total' => $total,
534 'success' => $success,
535 'failed' => $failed,
536 'last_run' => $last ?: null,
537 ) );
538 }
539
540 public static function clear_logs() {
541 global $wpdb;
542 $wpdb->query( 'TRUNCATE TABLE ' . Database::get_logs_table() );
543 return rest_ensure_response( array( 'success' => true ) );
544 }
545
546 /**
547 * Check permission for REST API
548 *
549 * @return bool
550 */
551 public static function check_permission() {
552 return current_user_can( 'manage_options' );
553 }
554
555 /**
556 * Get workflows
557 *
558 * @param \WP_REST_Request $request Request object.
559 * @return \WP_REST_Response
560 */
561 public static function get_workflows( $request ) {
562 global $wpdb;
563 $table = Database::get_workflows_table();
564
565 $workflows = $wpdb->get_results(
566 "SELECT * FROM {$table} ORDER BY created_at DESC",
567 ARRAY_A
568 );
569
570 // Decode workflow_data JSON.
571 foreach ( $workflows as &$workflow ) {
572 $workflow['workflow_data'] = json_decode( $workflow['workflow_data'], true );
573 }
574
575 return rest_ensure_response( $workflows );
576 }
577
578 /**
579 * Create workflow
580 *
581 * @param \WP_REST_Request $request Request object.
582 * @return \WP_REST_Response
583 */
584 public static function create_workflow( $request ) {
585 global $wpdb;
586 $table = Database::get_workflows_table();
587
588 $name = sanitize_text_field( $request->get_param( 'name' ) );
589 $description = sanitize_textarea_field( $request->get_param( 'description' ) );
590 $workflow_data = wp_json_encode( $request->get_param( 'workflow_data' ) );
591 $status = sanitize_text_field( $request->get_param( 'status' ) ) ?: 'active';
592
593 $result = $wpdb->insert(
594 $table,
595 array(
596 'name' => $name,
597 'description' => $description,
598 'workflow_data' => $workflow_data,
599 'status' => $status,
600 ),
601 array( '%s', '%s', '%s', '%s' )
602 );
603
604 if ( $result ) {
605 return rest_ensure_response(
606 array(
607 'success' => true,
608 'id' => $wpdb->insert_id,
609 )
610 );
611 }
612
613 return new \WP_Error( 'create_failed', __( 'Failed to create workflow', 'wpbot-automator' ), array( 'status' => 500 ) );
614 }
615
616 /**
617 * Update workflow
618 *
619 * @param \WP_REST_Request $request Request object.
620 * @return \WP_REST_Response
621 */
622 public static function update_workflow( $request ) {
623 global $wpdb;
624 $table = Database::get_workflows_table();
625
626 $id = absint( $request->get_param( 'id' ) );
627 $name = sanitize_text_field( $request->get_param( 'name' ) );
628 $description = sanitize_textarea_field( $request->get_param( 'description' ) );
629 $workflow_data = wp_json_encode( $request->get_param( 'workflow_data' ) );
630 $status = sanitize_text_field( $request->get_param( 'status' ) );
631
632 $result = $wpdb->update(
633 $table,
634 array(
635 'name' => $name,
636 'description' => $description,
637 'workflow_data' => $workflow_data,
638 'status' => $status,
639 ),
640 array( 'id' => $id ),
641 array( '%s', '%s', '%s', '%s' ),
642 array( '%d' )
643 );
644
645 if ( false !== $result ) {
646 return rest_ensure_response( array( 'success' => true ) );
647 }
648
649 return new \WP_Error( 'update_failed', __( 'Failed to update workflow', 'wpbot-automator' ), array( 'status' => 500 ) );
650 }
651
652 /**
653 * Delete workflow
654 *
655 * @param \WP_REST_Request $request Request object.
656 * @return \WP_REST_Response
657 */
658 public static function delete_workflow( $request ) {
659 global $wpdb;
660 $table = Database::get_workflows_table();
661
662 $id = absint( $request->get_param( 'id' ) );
663
664 $result = $wpdb->delete(
665 $table,
666 array( 'id' => $id ),
667 array( '%d' )
668 );
669
670 if ( $result ) {
671 return rest_ensure_response( array( 'success' => true ) );
672 }
673
674 return new \WP_Error( 'delete_failed', __( 'Failed to delete workflow', 'wpbot-automator' ), array( 'status' => 500 ) );
675 }
676
677 /**
678 * Get WooCommerce products
679 *
680 * @param \WP_REST_Request $request Request object.
681 * @return \WP_REST_Response
682 */
683 public static function get_products( $request ) {
684 if ( ! class_exists( 'WooCommerce' ) ) {
685 return rest_ensure_response( array() );
686 }
687
688 $args = array(
689 'limit' => -1,
690 'status' => 'publish',
691 );
692
693 $products = wc_get_products( $args );
694 $response = array();
695
696 foreach ( $products as $product ) {
697 $response[] = array(
698 'id' => $product->get_id(),
699 'name' => $product->get_name(),
700 );
701 }
702
703 return rest_ensure_response( $response );
704 }
705
706 /**
707 * Get list of WPForms forms.
708 */
709 public static function get_wpforms_forms() {
710 if ( ! function_exists( 'wpforms' ) ) {
711 return rest_ensure_response( array() );
712 }
713
714 $forms = wpforms()->form->get( '', array( 'fields' => 'id,post_title', 'posts_per_page' => -1 ) );
715 $response = array();
716
717 if ( is_array( $forms ) ) {
718 foreach ( $forms as $form ) {
719 $response[] = array(
720 'id' => (string) $form->ID,
721 'name' => $form->post_title,
722 );
723 }
724 }
725
726 return rest_ensure_response( $response );
727 }
728
729 /**
730 * Get list of Conversational forms.
731 */
732 public static function get_conversational_forms() {
733 if ( ! class_exists( 'Qcformbuilder_Forms' ) ) {
734 return rest_ensure_response( array() );
735 }
736
737 $forms = \Qcformbuilder_Forms::get_forms( true, false );
738 $response = array();
739
740 if ( is_array( $forms ) ) {
741 foreach ( $forms as $form_id => $form ) {
742 $response[] = array(
743 'id' => (string) $form_id,
744 'name' => isset( $form['name'] ) ? $form['name'] : $form_id,
745 );
746 }
747 }
748
749 return rest_ensure_response( $response );
750 }
751
752 /**
753 * Get list of Contact Form 7 forms.
754 */
755 public static function get_cf7_forms() {
756 if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
757 return rest_ensure_response( array() );
758 }
759
760 $forms = \WPCF7_ContactForm::find( array( 'posts_per_page' => -1 ) );
761 $response = array();
762
763 foreach ( $forms as $form ) {
764 $response[] = array(
765 'id' => (string) $form->id(),
766 'name' => $form->title(),
767 );
768 }
769
770 return rest_ensure_response( $response );
771 }
772
773 /**
774 * Test an action
775 *
776 * @param \WP_REST_Request $request Request object.
777 * @return \WP_REST_Response
778 */
779 public static function test_action( $request ) {
780 $app_id = sanitize_text_field( $request->get_param( 'appId' ) );
781 $action_id = sanitize_text_field( $request->get_param( 'actionId' ) );
782 $config = $request->get_param( 'config' );
783
784 $action_obj = Registry::get_action( $app_id );
785
786 if ( ! $action_obj ) {
787 return new \WP_Error( 'action_not_found', __( 'Action not found', 'wpbot-automator' ), array( 'status' => 404 ) );
788 }
789
790 // Prepare dummy trigger data for token parsing (optional).
791 $trigger_data = array(
792 'token' => 'sample_token_value',
793 'id' => 123,
794 'name' => 'Sample Name',
795 );
796
797 $action_data = array(
798 'actionId' => $action_id,
799 'config' => $config,
800 );
801
802 $result = $action_obj->execute( $action_data, $trigger_data );
803
804 return rest_ensure_response( $result );
805 }
806 /**
807 * Export workflows as a JSON download
808 *
809 * @param \WP_REST_Request $request Request object.
810 * @return void — sends headers and exits.
811 */
812 public static function export_workflows( $request ) {
813 global $wpdb;
814 $table = Database::get_workflows_table();
815
816 $id = absint( $request->get_param( 'id' ) );
817
818 if ( $id ) {
819 $rows = $wpdb->get_results(
820 $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ),
821 ARRAY_A
822 );
823 $filename = 'wpbot-workflow-' . $id . '.json';
824 } else {
825 $rows = $wpdb->get_results( "SELECT * FROM {$table} ORDER BY created_at DESC", ARRAY_A );
826 $filename = 'wpbot-workflows-all.json';
827 }
828
829 if ( empty( $rows ) ) {
830 wp_send_json_error( array( 'message' => 'No workflows found.' ), 404 );
831 return;
832 }
833
834 // Decode workflow_data so the exported JSON is human-readable.
835 foreach ( $rows as &$row ) {
836 $row['workflow_data'] = json_decode( $row['workflow_data'], true );
837 }
838 unset( $row );
839
840 $json = wp_json_encode( array( 'workflows' => $rows ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
841
842 header( 'Content-Type: application/json' );
843 header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
844 header( 'Cache-Control: no-cache, no-store, must-revalidate' );
845 echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
846 exit;
847 }
848
849 /**
850 * Import workflows from a JSON payload
851 *
852 * @param \WP_REST_Request $request Request object.
853 * @return \WP_REST_Response|\WP_Error
854 */
855 public static function import_workflows( $request ) {
856 global $wpdb;
857 $table = Database::get_workflows_table();
858
859 $workflows = $request->get_param( 'workflows' );
860
861 if ( empty( $workflows ) || ! is_array( $workflows ) ) {
862 return new \WP_Error(
863 'invalid_data',
864 __( 'Invalid import data. Expected a "workflows" array.', 'wpbot-automator' ),
865 array( 'status' => 400 )
866 );
867 }
868
869 $imported = 0;
870 $errors = array();
871
872 foreach ( $workflows as $index => $workflow ) {
873 $name = isset( $workflow['name'] ) ? sanitize_text_field( $workflow['name'] ) : '';
874 $description = isset( $workflow['description'] ) ? sanitize_textarea_field( $workflow['description'] ) : '';
875 $status = isset( $workflow['status'] ) ? sanitize_text_field( $workflow['status'] ) : 'inactive';
876 $workflow_data = isset( $workflow['workflow_data'] ) ? wp_json_encode( $workflow['workflow_data'] ) : wp_json_encode( array() );
877
878 if ( empty( $name ) ) {
879 $errors[] = "Workflow at index {$index} is missing a name — skipped.";
880 continue;
881 }
882
883 $result = $wpdb->insert(
884 $table,
885 array(
886 'name' => $name,
887 'description' => $description,
888 'status' => $status,
889 'workflow_data' => $workflow_data,
890 ),
891 array( '%s', '%s', '%s', '%s' )
892 );
893
894 if ( $result ) {
895 ++$imported;
896 } else {
897 $errors[] = "Failed to insert workflow \"{$name}\" (index {$index}).";
898 }
899 }
900
901 return rest_ensure_response(
902 array(
903 'success' => $imported > 0,
904 'imported' => $imported,
905 'errors' => $errors,
906 )
907 );
908 }
909
910 /**
911 * Send support email
912 *
913 * @param \WP_REST_Request $request Request object.
914 * @return \WP_REST_Response|\WP_Error
915 */
916 public static function send_support_email( $request ) {
917 $from_email = sanitize_email( $request->get_param( 'from_email' ) );
918 $message = sanitize_textarea_field( $request->get_param( 'message' ) );
919
920 if ( ! is_email( $from_email ) ) {
921 return new \WP_Error( 'invalid_email', __( 'Invalid email address', 'wpbot-automator' ), array( 'status' => 400 ) );
922 }
923
924 if ( empty( $message ) ) {
925 return new \WP_Error( 'empty_message', __( 'Message cannot be empty', 'wpbot-automator' ), array( 'status' => 400 ) );
926 }
927
928 $to = 'hello@wpbot.pro';
929 $subject = 'WPBot Automator Support Request';
930 $headers = array(
931 'Content-Type: text/html; charset=UTF-8',
932 'Reply-To: ' . $from_email,
933 'From: ' . $from_email,
934 );
935
936 $body = wpautop( $message );
937
938 $sent = wp_mail( $to, $subject, $body, $headers );
939
940 if ( $sent ) {
941 return rest_ensure_response( array( 'success' => true ) );
942 }
943
944 return new \WP_Error( 'email_failed', __( 'Failed to send support email', 'wpbot-automator' ), array( 'status' => 500 ) );
945 }
946
947 /**
948 * Get credentials
949 *
950 * @param \WP_REST_Request $request Request object.
951 * @return \WP_REST_Response
952 */
953 public static function get_credentials( $request ) {
954 $app_id = sanitize_text_field( $request->get_param( 'app_id' ) );
955 $credentials = get_option( 'wpbot_automator_credentials', array() );
956
957 if ( $app_id ) {
958 $filtered = array();
959 foreach ( $credentials as $cred ) {
960 if ( isset($cred['app_id']) && $cred['app_id'] === $app_id ) {
961 // We only send back public safe info to frontend (name, id, app_id).
962 $filtered[] = array(
963 'id' => $cred['id'],
964 'name' => $cred['name'],
965 'app_id' => $cred['app_id']
966 );
967 }
968 }
969 return rest_ensure_response( $filtered );
970 }
971
972 // If no app_id provided, return all credentials (names and IDs only)
973 $filtered = array();
974 foreach ( $credentials as $cred ) {
975 $filtered[] = array(
976 'id' => $cred['id'],
977 'name' => $cred['name'],
978 'app_id' => $cred['app_id']
979 );
980 }
981 return rest_ensure_response( $filtered );
982 }
983
984 /**
985 * Save a credential
986 *
987 * @param \WP_REST_Request $request Request object.
988 * @return \WP_REST_Response|\WP_Error
989 */
990 public static function save_credential( $request ) {
991 $name = sanitize_text_field( $request->get_param( 'name' ) );
992 $app_id = sanitize_text_field( $request->get_param( 'app_id' ) );
993 $data = $request->get_param( 'data' );
994
995 error_log('save_credential params: name=' . $name . ', app_id=' . $app_id . ', data=' . print_r($data, true));
996
997 if ( empty( $name ) || empty( $app_id ) || empty( $data ) ) {
998 error_log('save_credential missing data error');
999 return new \WP_Error( 'missing_data', __( 'Name, app_id, and data are required.', 'wpbot-automator' ), array( 'status' => 400 ) );
1000 }
1001
1002 $credentials = get_option( 'wpbot_automator_credentials', array() );
1003 if ( ! is_array( $credentials ) ) {
1004 $credentials = array();
1005 }
1006
1007 $new_id = uniqid( 'cred_' );
1008 $credentials[$new_id] = array(
1009 'id' => $new_id,
1010 'name' => $name,
1011 'app_id' => $app_id,
1012 'data' => $data,
1013 'created_at' => current_time( 'mysql' ),
1014 );
1015
1016 update_option( 'wpbot_automator_credentials', $credentials );
1017 error_log('save_credential saved: ' . $new_id);
1018
1019 return rest_ensure_response( array(
1020 'success' => true,
1021 'credential' => array(
1022 'id' => $new_id,
1023 'name' => $name,
1024 'app_id' => $app_id
1025 )
1026 ) );
1027 }
1028
1029 /**
1030 * Delete a credential
1031 *
1032 * @param \WP_REST_Request $request Request object.
1033 * @return \WP_REST_Response|\WP_Error
1034 */
1035 public static function delete_credential( $request ) {
1036 $id = sanitize_text_field( $request->get_param( 'id' ) );
1037 $credentials = get_option( 'wpbot_automator_credentials', array() );
1038
1039 if ( isset( $credentials[$id] ) ) {
1040 unset( $credentials[$id] );
1041 update_option( 'wpbot_automator_credentials', $credentials );
1042 return rest_ensure_response( array( 'success' => true ) );
1043 }
1044
1045 return new \WP_Error( 'not_found', __( 'Credential not found.', 'wpbot-automator' ), array( 'status' => 404 ) );
1046 }
1047
1048 /**
1049 * Get all email templates
1050 */
1051 public static function get_email_templates() {
1052 global $wpdb;
1053 $table = Database::get_email_templates_table();
1054
1055 if ( ! $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) ) {
1056 return rest_ensure_response( array() );
1057 }
1058
1059 $templates = $wpdb->get_results(
1060 "SELECT id, name FROM {$table} ORDER BY name ASC",
1061 ARRAY_A
1062 );
1063
1064 error_log( 'WPbot Automator - GET Templates - Table: ' . $table );
1065 error_log( 'WPbot Automator - GET Templates - Count: ' . count( $templates ) );
1066 error_log( 'WPbot Automator - GET Templates - JSON: ' . wp_json_encode( $templates ) );
1067
1068 return rest_ensure_response( is_array( $templates ) ? $templates : array() );
1069 }
1070
1071 /**
1072 * Get a single email template
1073 */
1074 public static function get_email_template( $request ) {
1075 global $wpdb;
1076 $id = absint( $request->get_param( 'id' ) );
1077 $table = Database::get_email_templates_table();
1078
1079 $template = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), ARRAY_A );
1080
1081 if ( ! $template ) {
1082 return new \WP_Error( 'not_found', __( 'Template not found', 'wpbot-automator' ), array( 'status' => 404 ) );
1083 }
1084
1085 return rest_ensure_response( $template );
1086 }
1087
1088 /**
1089 * Save an email template (Create or Update)
1090 */
1091 public static function save_email_template( $request ) {
1092 global $wpdb;
1093 $table = Database::get_email_templates_table();
1094
1095 $id = absint( $request->get_param( 'id' ) );
1096 $name = sanitize_text_field( $request->get_param( 'name' ) );
1097 $subject = sanitize_text_field( $request->get_param( 'subject' ) );
1098 $body_html = $request->get_param( 'body_html' ); // Allow HTML here.
1099 $body_json = $request->get_param( 'body_json' );
1100
1101 if ( empty( $name ) ) {
1102 return new \WP_Error( 'missing_name', __( 'Template name is required', 'wpbot-automator' ), array( 'status' => 400 ) );
1103 }
1104
1105 // Free plan: limit to 3 templates on creation.
1106 if ( ! $id ) {
1107 if ( ! function_exists( 'wpbot_automator_pro_init' ) ) {
1108 $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
1109 if ( $count >= 3 ) {
1110 return new \WP_Error(
1111 'template_limit_reached',
1112 __( 'Free plan is limited to 3 email templates. Upgrade to Pro for unlimited templates.', 'wpbot-automator' ),
1113 array( 'status' => 403 )
1114 );
1115 }
1116 }
1117 }
1118
1119 $data = array(
1120 'name' => $name,
1121 'subject' => $subject,
1122 'body_html' => $body_html,
1123 'body_json' => is_scalar( $body_json ) ? $body_json : wp_json_encode( $body_json ),
1124 );
1125
1126 error_log( 'WPbot Automator - Saving Template - ID: ' . $id );
1127 error_log( 'WPbot Automator - Saving Template - Data: ' . wp_json_encode( array_keys( $data ) ) );
1128
1129 if ( $id ) {
1130 $result = $wpdb->update( $table, $data, array( 'id' => $id ) );
1131 } else {
1132 $result = $wpdb->insert( $table, $data );
1133 $id = $wpdb->insert_id;
1134 }
1135
1136 if ( false === $result ) {
1137 error_log( 'WPbot Automator - Save Template FAILED. Error: ' . $wpdb->last_error );
1138 return new \WP_Error( 'save_failed', __( 'Failed to save template', 'wpbot-automator' ) . ': ' . $wpdb->last_error, array( 'status' => 500 ) );
1139 }
1140
1141 error_log( 'WPbot Automator - Save Template SUCCESS. ID: ' . $id );
1142
1143 return rest_ensure_response( array( 'success' => true, 'id' => $id ) );
1144 }
1145
1146 /**
1147 * Delete an email template
1148 */
1149 public static function delete_email_template( $request ) {
1150 global $wpdb;
1151 $table = Database::get_email_templates_table();
1152 $id = absint( $request->get_param( 'id' ) );
1153
1154 $result = $wpdb->delete( $table, array( 'id' => $id ) );
1155
1156 if ( ! $result ) {
1157 return new \WP_Error( 'delete_failed', __( 'Failed to delete template', 'wpbot-automator' ), array( 'status' => 500 ) );
1158 }
1159
1160 return rest_ensure_response( array( 'success' => true ) );
1161 }
1162
1163 /**
1164 * AI Workflow Generator — generate a workflow from a natural-language prompt.
1165 *
1166 * @param \WP_REST_Request $request
1167 */
1168 public static function generate_workflow( \WP_REST_Request $request ) {
1169 $provider = sanitize_text_field( $request->get_param( 'provider' ) );
1170 $api_key = sanitize_text_field( $request->get_param( 'api_key' ) );
1171 $model = sanitize_text_field( $request->get_param( 'model' ) );
1172 $user_prompt = sanitize_textarea_field( $request->get_param( 'prompt' ) );
1173
1174 if ( empty( $api_key ) || empty( $user_prompt ) ) {
1175 return new \WP_Error( 'missing_params', 'API key and prompt are required.', array( 'status' => 400 ) );
1176 }
1177
1178 $system_prompt = self::get_workflow_generator_system_prompt();
1179
1180 switch ( $provider ) {
1181 case 'openai':
1182 $raw = self::call_openai_for_workflow( $api_key, $model ?: 'gpt-4o-mini', $system_prompt, $user_prompt );
1183 break;
1184 case 'claude':
1185 $raw = self::call_claude_for_workflow( $api_key, $model ?: 'claude-3-5-sonnet-20240620', $system_prompt, $user_prompt );
1186 break;
1187 case 'gemini':
1188 $raw = self::call_gemini_for_workflow( $api_key, $model ?: 'gemini-1.5-flash', $system_prompt, $user_prompt );
1189 break;
1190 default:
1191 return new \WP_Error( 'invalid_provider', 'Invalid AI provider. Use openai, claude, or gemini.', array( 'status' => 400 ) );
1192 }
1193
1194 if ( is_wp_error( $raw ) ) {
1195 return $raw;
1196 }
1197
1198 // Strip markdown code fences if the model wrapped the JSON.
1199 $json_text = trim( $raw );
1200 $json_text = preg_replace( '/^```(?:json)?\s*/i', '', $json_text );
1201 $json_text = preg_replace( '/\s*```\s*$/', '', $json_text );
1202
1203 $workflow = json_decode( $json_text, true );
1204
1205 if ( JSON_ERROR_NONE !== json_last_error() || empty( $workflow['nodes'] ) || empty( $workflow['connections'] ) ) {
1206 return new \WP_Error( 'parse_error', 'AI returned invalid workflow data. Please try again with a clearer description.', array( 'status' => 422 ) );
1207 }
1208
1209 return rest_ensure_response( array(
1210 'success' => true,
1211 'workflow' => $workflow,
1212 ) );
1213 }
1214
1215 /**
1216 * System prompt sent to every AI provider.
1217 */
1218 private static function get_workflow_generator_system_prompt() {
1219 return 'You are a workflow automation assistant for WPBot Automator, a WordPress plugin similar to Zapier.
1220 Convert the user\'s plain-English description into a JSON workflow using ONLY the apps and action IDs listed below.
1221
1222 TRIGGER APPS (first node only, type="trigger"):
1223 - wordpress: wp_post_publish, wp_user_register, wp_user_login, wp_comment_added
1224 - woocommerce: wc_order_created, wc_order_status_change, wc_payment_completed
1225 - wpforms: wpforms_submit
1226 - cf7: cf7_submit
1227 - fluent_forms: ff_form_submit
1228 - webhook: incoming_webhook
1229 - facebook: facebook_lead_ads
1230
1231 ACTION APPS (all subsequent nodes, type="action"):
1232 - google_sheets: gs_add_row, gs_update_row
1233 - mail: send_email
1234 - telegram: tg_send_message
1235 - whatsapp: wa_send_message
1236 - facebook: fb_create_post
1237 - instagram: ig_publish_photo
1238 - linkedin: li_create_post
1239 - openai: chat_completion
1240 - claude: chat_completion
1241 - gemini: chat_completion
1242 - fluentcrm: create_contact
1243 - filters: filter_condition
1244 - iterator: iterator_loop
1245 - iterator_end: iterator_end
1246 - webhook: outgoing_webhook
1247 - api: api_request
1248 - mailrefine: mr_verify_email
1249
1250 Return ONLY valid JSON with no explanation, no markdown fences:
1251 {"name":"Short workflow name","description":"One sentence description","nodes":[{"type":"trigger","appId":"wpforms","actionId":"wpforms_submit"},{"type":"action","appId":"google_sheets","actionId":"gs_add_row"}],"connections":[{"from":0,"to":1}]}
1252
1253 Rules:
1254 - Exactly one trigger node at index 0
1255 - All other nodes are actions
1256 - "from" and "to" are zero-based node indices
1257 - Use ONLY the appId/actionId values from the lists above — never invent new ones
1258 - For parallel branches, multiple connection objects may share the same "from" value
1259 - Return ONLY the raw JSON object, nothing else';
1260 }
1261
1262 /**
1263 * Call OpenAI chat completions API.
1264 *
1265 * @return string|\WP_Error Raw text response or error.
1266 */
1267 private static function call_openai_for_workflow( $api_key, $model, $system_prompt, $user_prompt ) {
1268 $body = array(
1269 'model' => $model,
1270 'messages' => array(
1271 array( 'role' => 'system', 'content' => $system_prompt ),
1272 array( 'role' => 'user', 'content' => $user_prompt ),
1273 ),
1274 'temperature' => 0.2,
1275 'response_format' => array( 'type' => 'json_object' ),
1276 );
1277
1278 $response = wp_remote_post(
1279 'https://api.openai.com/v1/chat/completions',
1280 array(
1281 'timeout' => 60,
1282 'headers' => array(
1283 'Authorization' => 'Bearer ' . $api_key,
1284 'Content-Type' => 'application/json',
1285 ),
1286 'body' => wp_json_encode( $body ),
1287 )
1288 );
1289
1290 if ( is_wp_error( $response ) ) {
1291 return $response;
1292 }
1293
1294 $data = json_decode( wp_remote_retrieve_body( $response ), true );
1295 if ( ! empty( $data['error'] ) ) {
1296 return new \WP_Error( 'openai_error', $data['error']['message'] ?? 'OpenAI error', array( 'status' => 400 ) );
1297 }
1298
1299 return $data['choices'][0]['message']['content'] ?? '';
1300 }
1301
1302 /**
1303 * Call Anthropic Claude messages API.
1304 *
1305 * @return string|\WP_Error
1306 */
1307 private static function call_claude_for_workflow( $api_key, $model, $system_prompt, $user_prompt ) {
1308 $body = array(
1309 'model' => $model,
1310 'max_tokens' => 1024,
1311 'system' => $system_prompt,
1312 'messages' => array(
1313 array( 'role' => 'user', 'content' => $user_prompt ),
1314 ),
1315 );
1316
1317 $response = wp_remote_post(
1318 'https://api.anthropic.com/v1/messages',
1319 array(
1320 'timeout' => 60,
1321 'headers' => array(
1322 'x-api-key' => $api_key,
1323 'anthropic-version' => '2023-06-01',
1324 'Content-Type' => 'application/json',
1325 ),
1326 'body' => wp_json_encode( $body ),
1327 )
1328 );
1329
1330 if ( is_wp_error( $response ) ) {
1331 return $response;
1332 }
1333
1334 $data = json_decode( wp_remote_retrieve_body( $response ), true );
1335 if ( ! empty( $data['error'] ) ) {
1336 return new \WP_Error( 'claude_error', $data['error']['message'] ?? 'Claude error', array( 'status' => 400 ) );
1337 }
1338
1339 return $data['content'][0]['text'] ?? '';
1340 }
1341
1342 /**
1343 * Call Google Gemini generateContent API.
1344 *
1345 * @return string|\WP_Error
1346 */
1347 private static function call_gemini_for_workflow( $api_key, $model, $system_prompt, $user_prompt ) {
1348 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode( $model ) . ':generateContent?key=' . rawurlencode( $api_key );
1349 $body = array(
1350 'contents' => array(
1351 array(
1352 'role' => 'user',
1353 'parts' => array( array( 'text' => $system_prompt . "\n\nUser request: " . $user_prompt ) ),
1354 ),
1355 ),
1356 'generationConfig' => array(
1357 'temperature' => 0.2,
1358 'responseMimeType' => 'application/json',
1359 ),
1360 );
1361
1362 $response = wp_remote_post(
1363 $url,
1364 array(
1365 'timeout' => 60,
1366 'headers' => array( 'Content-Type' => 'application/json' ),
1367 'body' => wp_json_encode( $body ),
1368 )
1369 );
1370
1371 if ( is_wp_error( $response ) ) {
1372 return $response;
1373 }
1374
1375 $data = json_decode( wp_remote_retrieve_body( $response ), true );
1376 if ( ! empty( $data['error'] ) ) {
1377 return new \WP_Error( 'gemini_error', $data['error']['message'] ?? 'Gemini error', array( 'status' => 400 ) );
1378 }
1379
1380 return $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
1381 }
1382 }
1383