PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / trunk
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback vtrunk
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / third-party / forms / fluent-forms / class-avcf-abilities-fluent.php
atarim-visual-collaboration / third-party / forms / fluent-forms Last commit date
class-avcf-abilities-fluent-pro.php 3 weeks ago class-avcf-abilities-fluent.php 3 weeks ago class-avcf-fluent-detector.php 1 month ago class-avcf-fluent-helpers.php 3 weeks ago
class-avcf-abilities-fluent.php
369 lines
1 <?php
2 /**
3 * Fluent Forms — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface specialised to Fluent Forms and namespaced
6 * atarim/fluent-*. Fluent stores submissions in all editions, so entry
7 * abilities are not Pro-gated. Fluent-specific power abilities (form CRUD,
8 * submission status workflow, entry notes, notifications/confirmation settings,
9 * and integration-feed reads) live in the -pro file. Permissions use Fluent's
10 * capabilities with a manage_options fallback. Direct $wpdb on Fluent's schema;
11 * not runtime-tested here.
12 *
13 * @package atarim-visual-collaboration
14 */
15
16 if ( ! defined('ABSPATH') ) {
17 exit;
18 }
19
20 class AVCF_Abilities_Fluent extends AVCF_Abilities_Base {
21
22 /** @var AVCF_Fluent_Detector */
23 private $detector;
24
25 public function __construct() {
26 $this->detector = new AVCF_Fluent_Detector();
27 }
28
29 public function register() {
30 if ( ! $this->detector->avcf_fluent_is_available() ) {
31 return;
32 }
33 $this->register_list_forms();
34 $this->register_get_form();
35 $this->register_list_entries();
36 $this->register_get_entry();
37 $this->register_get_form_stats();
38 $this->register_get_form_spam_stats();
39 $this->register_find_silent_forms();
40 $this->register_export_entries();
41 $this->register_mark_entry_spam();
42 $this->register_update_notifications();
43 $this->register_duplicate_form();
44 }
45
46 /* ------------------------------ shared ----------------------------- */
47
48 private function ro_meta() { return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ]; }
49 private function write_meta( $d = false ) { return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $d, 'idempotent' => false ] ]; }
50
51 public function can_view_forms() { return current_user_can( 'fluentform_forms_manager' ) || current_user_can( 'fluentform_view_dashboard' ) || current_user_can( 'manage_options' ); }
52 public function can_view_entries() { return current_user_can( 'fluentform_entries_viewer' ) || current_user_can( 'fluentform_view_dashboard' ) || current_user_can( 'manage_options' ); }
53 public function can_edit_forms() { return current_user_can( 'fluentform_forms_manager' ) || current_user_can( 'manage_options' ); }
54 public function can_edit_entries() { return current_user_can( 'fluentform_entries_viewer' ) || current_user_can( 'manage_options' ); }
55
56 /* ---------------------------- list-forms --------------------------- */
57
58 private function register_list_forms() {
59 $self = $this;
60 wp_register_ability( 'atarim/fluent-list-forms', [
61 'label' => 'List Fluent Forms', 'category' => 'atarim',
62 'description' => 'List Fluent Forms: { id, title, status, entries_total, last_submission, created_at }. Optional search, status filter (published|unpublished, default all), limit, offset.',
63 'input_schema' => [ 'type' => 'object', 'properties' => [ 'search' => [ 'type' => 'string' ], 'status' => [ 'type' => 'string', 'enum' => [ 'published', 'unpublished' ] ], 'limit' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, 'default' => 50 ], 'offset' => [ 'type' => 'integer', 'minimum' => 0, 'default' => 0 ] ], 'additionalProperties' => false ],
64 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'forms' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
65 'execute_callback' => function( $input = [] ) {
66 global $wpdb;
67 $table = AVCF_Fluent_Helpers::forms_table();
68 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
69 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
70 $where = [ '1=1' ]; $params = [];
71 if ( isset( $input['status'] ) && $input['status'] !== '' ) { $where[] = 'status = %s'; $params[] = (string) $input['status']; }
72 if ( isset( $input['search'] ) && $input['search'] !== '' ) { $where[] = 'title LIKE %s'; $params[] = '%' . $wpdb->esc_like( (string) $input['search'] ) . '%'; }
73 $where_sql = implode( ' AND ', $where );
74 $total = (int) $wpdb->get_var( $params ? $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE {$where_sql}", $params ) : "SELECT COUNT(*) FROM `{$table}` WHERE {$where_sql}" );
75 $sql = "SELECT id, title, status, created_at, updated_at FROM `{$table}` WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
76 $rows = $wpdb->get_results( $wpdb->prepare( $sql, array_merge( $params, [ $limit, $offset ] ) ) );
77 $out = [];
78 foreach ( (array) $rows as $r ) { $out[] = AVCF_Fluent_Helpers::map_form_summary( $r ); }
79 return [ 'success' => true, 'forms' => $out, 'total' => $total, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
80 },
81 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
82 'meta' => $this->ro_meta(),
83 ] );
84 }
85
86 /* ----------------------------- get-form ---------------------------- */
87
88 private function register_get_form() {
89 $self = $this;
90 wp_register_ability( 'atarim/fluent-get-form', [
91 'label' => 'Get Fluent Form', 'category' => 'atarim',
92 'description' => 'Full Fluent form definition: flattened fields (id=field name, type, label, required, options), notifications, shortcode. include_raw:true adds the raw form row.',
93 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_raw' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
94 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'form' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
95 'execute_callback' => function( $input = [] ) {
96 global $wpdb;
97 $table = AVCF_Fluent_Helpers::forms_table();
98 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE id = %d", (int) $input['form_id'] ) );
99 if ( ! $row ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
100 return [ 'success' => true, 'form' => AVCF_Fluent_Helpers::map_form_full( $row, ! empty( $input['include_raw'] ) ), 'message' => 'OK.' ];
101 },
102 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
103 'meta' => $this->ro_meta(),
104 ] );
105 }
106
107 /* --------------------------- list-entries -------------------------- */
108
109 private function register_list_entries() {
110 $self = $this;
111 wp_register_ability( 'atarim/fluent-list-entries', [
112 'label' => 'List Fluent Entries', 'category' => 'atarim',
113 'description' => 'List submissions for a form (newest first), normalized to { id, submitted_at, status, fields[] } with field labels resolved. Filters: date_from, date_to, include_spam. Paged via limit/offset.',
114 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ], 'include_spam' => [ 'type' => 'boolean', 'default' => false ], 'limit' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, 'default' => 50 ], 'offset' => [ 'type' => 'integer', 'minimum' => 0, 'default' => 0 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
115 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
116 'execute_callback' => function( $input = [] ) {
117 global $wpdb;
118 $form_id = (int) $input['form_id'];
119 $subs = AVCF_Fluent_Helpers::subs_table();
120 $forms = AVCF_Fluent_Helpers::forms_table();
121 $where = [ 'form_id = %d' ]; $params = [ $form_id ];
122 if ( empty( $input['include_spam'] ) ) { $where[] = 'status != %s'; $params[] = 'spam'; }
123 if ( ! empty( $input['date_from'] ) ) { $where[] = 'created_at >= %s'; $params[] = (string) $input['date_from']; }
124 if ( ! empty( $input['date_to'] ) ) { $where[] = 'created_at <= %s'; $params[] = (string) $input['date_to']; }
125 $where_sql = implode( ' AND ', $where );
126 $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$subs}` WHERE {$where_sql}", $params ) );
127 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
128 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
129 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$subs}` WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d", array_merge( $params, [ $limit, $offset ] ) ) );
130 $form_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$forms}` WHERE id = %d", $form_id ) );
131 $label_map = $form_row ? AVCF_Fluent_Helpers::build_label_map( AVCF_Fluent_Helpers::fields_from_row( $form_row ) ) : [];
132 $out = [];
133 foreach ( (array) $rows as $r ) { $out[] = AVCF_Fluent_Helpers::normalize_entry( $r, $form_id, $label_map, false ); }
134 return [ 'success' => true, 'entries' => $out, 'total' => $total, 'message' => sprintf( '%d entr(y/ies).', count( $out ) ) ];
135 },
136 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
137 'meta' => $this->ro_meta(),
138 ] );
139 }
140
141 /* ---------------------------- get-entry ---------------------------- */
142
143 private function register_get_entry() {
144 $self = $this;
145 wp_register_ability( 'atarim/fluent-get-entry', [
146 'label' => 'Get Fluent Entry', 'category' => 'atarim',
147 'description' => 'Full normalized submission by id, with field labels resolved and the raw row included.',
148 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
149 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entry' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
150 'execute_callback' => function( $input = [] ) {
151 global $wpdb;
152 $subs = AVCF_Fluent_Helpers::subs_table();
153 $forms = AVCF_Fluent_Helpers::forms_table();
154 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$subs}` WHERE id = %d", (int) $input['entry_id'] ) );
155 if ( ! $row ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
156 $form_id = (int) $row->form_id;
157 $form_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$forms}` WHERE id = %d", $form_id ) );
158 $label_map = $form_row ? AVCF_Fluent_Helpers::build_label_map( AVCF_Fluent_Helpers::fields_from_row( $form_row ) ) : [];
159 return [ 'success' => true, 'entry' => AVCF_Fluent_Helpers::normalize_entry( $row, $form_id, $label_map, true ), 'message' => 'OK.' ];
160 },
161 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
162 'meta' => $this->ro_meta(),
163 ] );
164 }
165
166 /* -------------------------- get-form-stats ------------------------- */
167
168 private function register_get_form_stats() {
169 $self = $this;
170 wp_register_ability( 'atarim/fluent-get-form-stats', [
171 'label' => 'Get Fluent Form Stats', 'category' => 'atarim',
172 'description' => 'Submission stats (non-spam): total, first/last submission, per-day counts. Optional date window.',
173 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
174 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
175 'execute_callback' => function( $input = [] ) {
176 return [ 'success' => true, 'stats' => AVCF_Fluent_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
177 },
178 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
179 'meta' => $this->ro_meta(),
180 ] );
181 }
182
183 /* ------------------------ get-form-spam-stats ---------------------- */
184
185 private function register_get_form_spam_stats() {
186 $self = $this;
187 wp_register_ability( 'atarim/fluent-get-form-spam-stats', [
188 'label' => 'Get Fluent Form Spam Stats', 'category' => 'atarim',
189 'description' => 'Spam vs non-spam submission counts and spam rate for a form.',
190 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
191 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
192 'execute_callback' => function( $input = [] ) {
193 $form_id = (int) $input['form_id'];
194 $all = AVCF_Fluent_Helpers::count_entries( $form_id, true );
195 $active = AVCF_Fluent_Helpers::count_entries( $form_id, false );
196 $spam = max( 0, $all - $active );
197 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $all > 0 ? round( $spam / $all, 4 ) : 0, 'message' => sprintf( '%d spam / %d active.', $spam, $active ) ];
198 },
199 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
200 'meta' => $this->ro_meta(),
201 ] );
202 }
203
204 /* ------------------------- find-silent-forms ----------------------- */
205
206 private function register_find_silent_forms() {
207 $self = $this;
208 wp_register_ability( 'atarim/fluent-find-silent-forms', [
209 'label' => 'Find Silent Fluent Forms', 'category' => 'atarim',
210 'description' => 'Forms with no submission in the last N days (default 30). Returns each with last_submission and days_silent.',
211 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
212 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
213 'execute_callback' => function( $input = [] ) {
214 global $wpdb;
215 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
216 $cutoff = time() - ( $days * DAY_IN_SECONDS );
217 $table = AVCF_Fluent_Helpers::forms_table();
218 $rows = $wpdb->get_results( "SELECT id, title FROM `{$table}` ORDER BY id DESC LIMIT 500" );
219 $silent = [];
220 foreach ( (array) $rows as $r ) {
221 $fid = (int) $r->id;
222 $last = AVCF_Fluent_Helpers::last_submission_at( $fid );
223 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
224 if ( $last_ts === false ) { $last_ts = 0; }
225 if ( $last_ts < $cutoff ) {
226 $silent[] = [ 'id' => $fid, 'title' => (string) $r->title, 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
227 }
228 }
229 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
230 },
231 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
232 'meta' => $this->ro_meta(),
233 ] );
234 }
235
236 /* -------------------------- export-entries ------------------------- */
237
238 private function register_export_entries() {
239 $self = $this;
240 wp_register_ability( 'atarim/fluent-export-entries', [
241 'label' => 'Export Fluent Entries', 'category' => 'atarim',
242 'description' => 'Export a form\'s submissions as CSV text. Capped via limit (default 1000). Excludes spam unless include_spam:true.',
243 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_spam' => [ 'type' => 'boolean', 'default' => false ], 'limit' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 5000, 'default' => 1000 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
244 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
245 'execute_callback' => function( $input = [] ) {
246 global $wpdb;
247 $form_id = (int) $input['form_id'];
248 $subs = AVCF_Fluent_Helpers::subs_table();
249 $forms = AVCF_Fluent_Helpers::forms_table();
250 $where = [ 'form_id = %d' ]; $params = [ $form_id ];
251 if ( empty( $input['include_spam'] ) ) { $where[] = 'status != %s'; $params[] = 'spam'; }
252 $where_sql = implode( ' AND ', $where );
253 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000;
254 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$subs}` WHERE {$where_sql} ORDER BY id DESC LIMIT %d", array_merge( $params, [ $limit ] ) ) );
255 $form_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$forms}` WHERE id = %d", $form_id ) );
256 $label_map = $form_row ? AVCF_Fluent_Helpers::build_label_map( AVCF_Fluent_Helpers::fields_from_row( $form_row ) ) : [];
257 $header = [ 'Entry ID', 'Submitted' ]; $header_set = false; $body = [];
258 foreach ( (array) $rows as $r ) {
259 $n = AVCF_Fluent_Helpers::normalize_entry( $r, $form_id, $label_map, false );
260 $line = [ (string) $n['id'], (string) $n['submitted_at'] ];
261 foreach ( $n['fields'] as $fld ) {
262 if ( ! $header_set ) { $header[] = $fld['label'] !== '' ? $fld['label'] : $fld['id']; }
263 $line[] = is_array( $fld['value'] ) ? implode( ' | ', array_map( 'strval', $fld['value'] ) ) : (string) $fld['value'];
264 }
265 $header_set = true; $body[] = $line;
266 }
267 $lines = [ self::csv_row( $header ) ];
268 foreach ( $body as $b ) { $lines[] = self::csv_row( $b ); }
269 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $body ), 'message' => sprintf( '%d row(s) exported.', count( $body ) ) ];
270 },
271 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
272 'meta' => $this->ro_meta(),
273 ] );
274 }
275
276 private static function csv_row( $cells ) {
277 $out = [];
278 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
279 return implode( ',', $out );
280 }
281
282 /* -------------------------- mark-entry-spam ------------------------ */
283
284 private function register_mark_entry_spam() {
285 $self = $this;
286 wp_register_ability( 'atarim/fluent-mark-entry-spam', [
287 'label' => 'Mark Fluent Entry Spam', 'category' => 'atarim',
288 'description' => 'Flag a submission as spam (or clear with is_spam:false, restoring it to unread).',
289 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
290 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
291 'execute_callback' => function( $input = [] ) {
292 global $wpdb;
293 $table = AVCF_Fluent_Helpers::subs_table();
294 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
295 $r = $wpdb->update( $table, [ 'status' => $is_spam ? 'spam' : 'unread' ], [ 'id' => (int) $input['entry_id'] ], [ '%s' ], [ '%d' ] );
296 if ( $r === false ) { return [ 'success' => false, 'message' => 'Update failed.' ]; }
297 return [ 'success' => true, 'message' => $is_spam ? 'Entry marked as spam.' : 'Entry unmarked from spam.' ];
298 },
299 'permission_callback' => function() use ( $self ) { return $self->can_edit_entries(); },
300 'meta' => $this->write_meta( false ),
301 ] );
302 }
303
304 /* ------------------------ update-notifications ---------------------- */
305
306 private function register_update_notifications() {
307 $self = $this;
308 wp_register_ability( 'atarim/fluent-update-notifications', [
309 'label' => 'Update Fluent Notification Recipients', 'category' => 'atarim',
310 'description' => 'Update recipient email(s) of one or more of a form\'s notifications. updates: [{ notification_id, recipients: [emails] }]. Emails sanitized; invalid sets skipped and reported.',
311 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'updates' => [ 'type' => 'array', 'items' => [ 'type' => 'object' ], 'minItems' => 1 ] ], 'required' => [ 'form_id', 'updates' ], 'additionalProperties' => false ],
312 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
313 'execute_callback' => function( $input = [] ) {
314 global $wpdb;
315 $meta_table = AVCF_Fluent_Helpers::meta_table();
316 $row = $wpdb->get_row( $wpdb->prepare( "SELECT id, value FROM `{$meta_table}` WHERE form_id = %d AND meta_key = %s LIMIT 1", (int) $input['form_id'], 'notifications' ) );
317 if ( ! $row ) { return [ 'success' => false, 'message' => 'No notifications configured on this form.' ]; }
318 $notifs = json_decode( $row->value, true );
319 if ( ! is_array( $notifs ) ) { return [ 'success' => false, 'message' => 'Notifications data is corrupt.' ]; }
320 $updated = []; $skipped = [];
321 foreach ( (array) $input['updates'] as $u ) {
322 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
323 if ( ! isset( $notifs[ $nid ] ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Not found on this form.' ]; continue; }
324 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
325 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; continue; }
326 $email_str = implode( ',', $clean );
327 if ( isset( $notifs[ $nid ]['sendTo'] ) && is_array( $notifs[ $nid ]['sendTo'] ) ) { $notifs[ $nid ]['sendTo']['email'] = $email_str; }
328 else { $notifs[ $nid ]['email'] = $email_str; }
329 $updated[] = $nid;
330 }
331 if ( ! empty( $updated ) ) { $wpdb->update( $meta_table, [ 'value' => wp_json_encode( $notifs ) ], [ 'id' => (int) $row->id ], [ '%s' ], [ '%d' ] ); }
332 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
333 },
334 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
335 'meta' => $this->write_meta( false ),
336 ] );
337 }
338
339 /* -------------------------- duplicate-form ------------------------- */
340
341 private function register_duplicate_form() {
342 $self = $this;
343 wp_register_ability( 'atarim/fluent-duplicate-form', [
344 'label' => 'Duplicate Fluent Form', 'category' => 'atarim',
345 'description' => 'Clone a form (row + all form_meta, including notifications). Optional new_title (defaults to "<title> (Copy)").',
346 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
347 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
348 'execute_callback' => function( $input = [] ) {
349 global $wpdb;
350 $form_id = (int) $input['form_id'];
351 $table = AVCF_Fluent_Helpers::forms_table();
352 $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE id = %d", $form_id ), ARRAY_A );
353 if ( ! $row ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
354 $title = isset( $input['new_title'] ) && $input['new_title'] !== '' ? sanitize_text_field( $input['new_title'] ) : ( (string) $row['title'] . ' (Copy)' );
355 $new = $row; unset( $new['id'] );
356 $new['title'] = $title; $new['created_at'] = current_time( 'mysql' ); $new['updated_at'] = current_time( 'mysql' );
357 if ( ! $wpdb->insert( $table, $new ) ) { return [ 'success' => false, 'message' => 'Duplicate failed.' ]; }
358 $new_id = (int) $wpdb->insert_id;
359 $meta_table = AVCF_Fluent_Helpers::meta_table();
360 $metas = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, value FROM `{$meta_table}` WHERE form_id = %d", $form_id ), ARRAY_A );
361 foreach ( (array) $metas as $m ) { $wpdb->insert( $meta_table, [ 'form_id' => $new_id, 'meta_key' => $m['meta_key'], 'value' => $m['value'] ] ); }
362 return [ 'success' => true, 'new_form_id' => $new_id, 'message' => sprintf( 'Duplicated as form %d.', $new_id ) ];
363 },
364 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
365 'meta' => $this->write_meta( false ),
366 ] );
367 }
368 }
369