PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.1
8.8.1 8.8.0 8.7.9 8.7.8 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 All 537 releases
← All changes | addons/automator/includes/core/plugin.php +1382 -1382 8.8.0 → 8.8.1 View file →
@@ -1,1382 +1,1382 @@
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 = '[email protected]';
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 -}
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 = '[email protected]';
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 +}