PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.0
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.0
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 / formidable / class-avcf-abilities-formidable.php
atarim-visual-collaboration / third-party / forms / formidable Last commit date
class-avcf-abilities-formidable-pro.php 1 month ago class-avcf-abilities-formidable.php 1 month ago class-avcf-formidable-detector.php 1 month ago class-avcf-formidable-helpers.php 1 month ago
class-avcf-abilities-formidable.php
364 lines
1 <?php
2 /**
3 * Formidable Forms — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface specialised to Formidable and namespaced
6 * atarim/formidable-*. Forms/fields/notifications/entries go through
7 * Formidable's PHP API; stats/spam use $wpdb on frm_items. Entries are stored
8 * by Formidable core, so they are not Pro-gated. Formidable-specific power
9 * (form CRUD, entry delete/status, notification action management, view-data
10 * helpers) lives in the -pro file. Permissions use Formidable's capabilities
11 * with a manage_options fallback. Built on Formidable's API; not runtime-tested.
12 *
13 * @package atarim-visual-collaboration
14 */
15
16 if ( ! defined('ABSPATH') ) {
17 exit;
18 }
19
20 class AVCF_Abilities_Formidable extends AVCF_Abilities_Base {
21
22 /** @var AVCF_Formidable_Detector */
23 private $detector;
24
25 public function __construct() {
26 $this->detector = new AVCF_Formidable_Detector();
27 }
28
29 public function register() {
30 if ( ! $this->detector->avcf_formidable_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( 'frm_edit_forms' ) || current_user_can( 'frm_view_forms' ) || current_user_can( 'manage_options' ); }
52 public function can_view_entries() { return current_user_can( 'frm_view_entries' ) || current_user_can( 'frm_edit_entries' ) || current_user_can( 'manage_options' ); }
53 public function can_edit_forms() { return current_user_can( 'frm_edit_forms' ) || current_user_can( 'manage_options' ); }
54 public function can_edit_entries() { return current_user_can( 'frm_edit_entries' ) || current_user_can( 'frm_delete_entries' ) || current_user_can( 'manage_options' ); }
55
56 /** Resolve label map for a form (public so closures can call). */
57 public function label_map_for( $form_id ) {
58 if ( ! class_exists( 'FrmForm' ) ) { return []; }
59 $form = FrmForm::getOne( (int) $form_id );
60 if ( ! $form ) { return []; }
61 return AVCF_Formidable_Helpers::build_label_map( AVCF_Formidable_Helpers::map_fields( (int) $form_id ) );
62 }
63
64 /* ---------------------------- list-forms --------------------------- */
65
66 private function register_list_forms() {
67 $self = $this;
68 wp_register_ability( 'atarim/formidable-list-forms', [
69 'label' => 'List Formidable Forms', 'category' => 'atarim',
70 'description' => 'List Formidable forms: { id, title, status, entries_total, last_submission, created_at }. Optional search (name), limit, offset.',
71 'input_schema' => [ 'type' => 'object', 'properties' => [ 'search' => [ 'type' => 'string' ], 'limit' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, 'default' => 50 ], 'offset' => [ 'type' => 'integer', 'minimum' => 0, 'default' => 0 ] ], 'additionalProperties' => false ],
72 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'forms' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
73 'execute_callback' => function( $input = [] ) {
74 $all = FrmForm::getAll( [ 'parent_form_id' => null, 'status' => 'published' ], 'name', '' );
75 if ( ! is_array( $all ) ) { $all = []; }
76 $search = isset( $input['search'] ) ? trim( (string) $input['search'] ) : '';
77 if ( $search !== '' ) {
78 $needle = strtolower( $search );
79 $all = array_values( array_filter( $all, function( $f ) use ( $needle ) { return isset( $f->name ) && strpos( strtolower( (string) $f->name ), $needle ) !== false; } ) );
80 }
81 $total = count( $all );
82 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
83 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
84 $page = array_slice( $all, $offset, $limit );
85 $out = [];
86 foreach ( $page as $f ) { $out[] = AVCF_Formidable_Helpers::map_form_summary( $f ); }
87 return [ 'success' => true, 'forms' => $out, 'total' => $total, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
88 },
89 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
90 'meta' => $this->ro_meta(),
91 ] );
92 }
93
94 /* ----------------------------- get-form ---------------------------- */
95
96 private function register_get_form() {
97 $self = $this;
98 wp_register_ability( 'atarim/formidable-get-form', [
99 'label' => 'Get Formidable Form', 'category' => 'atarim',
100 'description' => 'Full Formidable form definition: fields (id, key, type, label, required, options), email-action notifications, shortcode. include_raw:true adds the form options array.',
101 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_raw' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
102 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'form' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
103 'execute_callback' => function( $input = [] ) {
104 $form = FrmForm::getOne( (int) $input['form_id'] );
105 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
106 return [ 'success' => true, 'form' => AVCF_Formidable_Helpers::map_form_full( $form, ! empty( $input['include_raw'] ) ), 'message' => 'OK.' ];
107 },
108 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
109 'meta' => $this->ro_meta(),
110 ] );
111 }
112
113 /* --------------------------- list-entries -------------------------- */
114
115 private function register_list_entries() {
116 $self = $this;
117 wp_register_ability( 'atarim/formidable-list-entries', [
118 'label' => 'List Formidable Entries', 'category' => 'atarim',
119 'description' => 'List entries for a form (newest first), normalized to { id, item_key, submitted_at, status, fields[] } with labels resolved. Filters: date_from, date_to, include_spam. Paged via limit/offset.',
120 '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 ],
121 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
122 'execute_callback' => function( $input = [] ) use ( $self ) {
123 if ( ! class_exists( 'FrmEntry' ) ) { return [ 'success' => false, 'message' => 'FrmEntry API unavailable.' ]; }
124 global $wpdb;
125 $form_id = (int) $input['form_id'];
126 $table = AVCF_Formidable_Helpers::items_table();
127 $where = [ 'form_id = %d' ]; $params = [ $form_id ];
128 if ( empty( $input['include_spam'] ) ) { $where[] = '(is_draft != 2 OR is_draft IS NULL)'; }
129 if ( ! empty( $input['date_from'] ) ) { $where[] = 'created_at >= %s'; $params[] = (string) $input['date_from']; }
130 if ( ! empty( $input['date_to'] ) ) { $where[] = 'created_at <= %s'; $params[] = (string) $input['date_to']; }
131 $where_sql = implode( ' AND ', $where );
132 $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE {$where_sql}", $params ) );
133 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
134 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
135 $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM `{$table}` WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d", array_merge( $params, [ $limit, $offset ] ) ) );
136 $label_map = $self->label_map_for( $form_id );
137 $out = [];
138 foreach ( (array) $ids as $eid ) {
139 $entry = FrmEntry::getOne( (int) $eid, true );
140 if ( $entry ) { $out[] = AVCF_Formidable_Helpers::normalize_entry( $entry, $form_id, $label_map, false ); }
141 }
142 return [ 'success' => true, 'entries' => $out, 'total' => $total, 'message' => sprintf( '%d entr(y/ies).', count( $out ) ) ];
143 },
144 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
145 'meta' => $this->ro_meta(),
146 ] );
147 }
148
149 /* ---------------------------- get-entry ---------------------------- */
150
151 private function register_get_entry() {
152 $self = $this;
153 wp_register_ability( 'atarim/formidable-get-entry', [
154 'label' => 'Get Formidable Entry', 'category' => 'atarim',
155 'description' => 'Full normalized entry by id, with field values, labels resolved, and the raw entry included.',
156 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
157 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entry' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
158 'execute_callback' => function( $input = [] ) use ( $self ) {
159 if ( ! class_exists( 'FrmEntry' ) ) { return [ 'success' => false, 'message' => 'FrmEntry API unavailable.' ]; }
160 $entry = FrmEntry::getOne( (int) $input['entry_id'], true );
161 if ( ! $entry ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
162 $form_id = isset( $entry->form_id ) ? (int) $entry->form_id : 0;
163 return [ 'success' => true, 'entry' => AVCF_Formidable_Helpers::normalize_entry( $entry, $form_id, $self->label_map_for( $form_id ), true ), 'message' => 'OK.' ];
164 },
165 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
166 'meta' => $this->ro_meta(),
167 ] );
168 }
169
170 /* -------------------------- get-form-stats ------------------------- */
171
172 private function register_get_form_stats() {
173 $self = $this;
174 wp_register_ability( 'atarim/formidable-get-form-stats', [
175 'label' => 'Get Formidable Form Stats', 'category' => 'atarim',
176 'description' => 'Submission stats (non-spam): total, first/last submission, per-day counts. Optional date window.',
177 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
178 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
179 'execute_callback' => function( $input = [] ) {
180 return [ 'success' => true, 'stats' => AVCF_Formidable_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
181 },
182 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
183 'meta' => $this->ro_meta(),
184 ] );
185 }
186
187 /* ------------------------ get-form-spam-stats ---------------------- */
188
189 private function register_get_form_spam_stats() {
190 $self = $this;
191 wp_register_ability( 'atarim/formidable-get-form-spam-stats', [
192 'label' => 'Get Formidable Form Spam Stats', 'category' => 'atarim',
193 'description' => 'Spam vs non-spam entry counts and spam rate (is_draft=2 is spam).',
194 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
195 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
196 'execute_callback' => function( $input = [] ) {
197 $form_id = (int) $input['form_id'];
198 $all = AVCF_Formidable_Helpers::count_entries( $form_id, true );
199 $active = AVCF_Formidable_Helpers::count_entries( $form_id, false );
200 $spam = max( 0, $all - $active );
201 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $all > 0 ? round( $spam / $all, 4 ) : 0, 'message' => sprintf( '%d spam / %d active.', $spam, $active ) ];
202 },
203 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
204 'meta' => $this->ro_meta(),
205 ] );
206 }
207
208 /* ------------------------- find-silent-forms ----------------------- */
209
210 private function register_find_silent_forms() {
211 $self = $this;
212 wp_register_ability( 'atarim/formidable-find-silent-forms', [
213 'label' => 'Find Silent Formidable Forms', 'category' => 'atarim',
214 'description' => 'Forms with no submission in the last N days (default 30). Returns each with last_submission and days_silent.',
215 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
216 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
217 'execute_callback' => function( $input = [] ) {
218 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
219 $cutoff = time() - ( $days * DAY_IN_SECONDS );
220 $all = FrmForm::getAll( [ 'parent_form_id' => null, 'status' => 'published' ], 'name', '' );
221 if ( ! is_array( $all ) ) { $all = []; }
222 $silent = [];
223 foreach ( $all as $f ) {
224 $fid = isset( $f->id ) ? (int) $f->id : 0;
225 if ( $fid <= 0 ) { continue; }
226 $last = AVCF_Formidable_Helpers::last_submission_at( $fid );
227 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
228 if ( $last_ts === false ) { $last_ts = 0; }
229 if ( $last_ts < $cutoff ) {
230 $silent[] = [ 'id' => $fid, 'title' => isset( $f->name ) ? (string) $f->name : '', 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
231 }
232 }
233 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
234 },
235 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
236 'meta' => $this->ro_meta(),
237 ] );
238 }
239
240 /* -------------------------- export-entries ------------------------- */
241
242 private function register_export_entries() {
243 $self = $this;
244 wp_register_ability( 'atarim/formidable-export-entries', [
245 'label' => 'Export Formidable Entries', 'category' => 'atarim',
246 'description' => 'Export a form\'s entries as CSV text. Capped via limit (default 1000). Excludes spam unless include_spam:true.',
247 '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 ],
248 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
249 'execute_callback' => function( $input = [] ) use ( $self ) {
250 if ( ! class_exists( 'FrmEntry' ) ) { return [ 'success' => false, 'message' => 'FrmEntry API unavailable.' ]; }
251 global $wpdb;
252 $form_id = (int) $input['form_id'];
253 $table = AVCF_Formidable_Helpers::items_table();
254 $where = [ 'form_id = %d' ]; $params = [ $form_id ];
255 if ( empty( $input['include_spam'] ) ) { $where[] = '(is_draft != 2 OR is_draft IS NULL)'; }
256 $where_sql = implode( ' AND ', $where );
257 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000;
258 $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM `{$table}` WHERE {$where_sql} ORDER BY id DESC LIMIT %d", array_merge( $params, [ $limit ] ) ) );
259 $label_map = $self->label_map_for( $form_id );
260 $header = [ 'Entry ID', 'Submitted' ]; $header_set = false; $body = [];
261 foreach ( (array) $ids as $eid ) {
262 $entry = FrmEntry::getOne( (int) $eid, true );
263 if ( ! $entry ) { continue; }
264 $n = AVCF_Formidable_Helpers::normalize_entry( $entry, $form_id, $label_map, false );
265 $line = [ (string) $n['id'], (string) $n['submitted_at'] ];
266 foreach ( $n['fields'] as $fld ) {
267 if ( ! $header_set ) { $header[] = $fld['label'] !== '' ? $fld['label'] : $fld['id']; }
268 $line[] = is_array( $fld['value'] ) ? implode( ' | ', array_map( 'strval', $fld['value'] ) ) : (string) $fld['value'];
269 }
270 $header_set = true; $body[] = $line;
271 }
272 $lines = [ self::csv_row( $header ) ];
273 foreach ( $body as $b ) { $lines[] = self::csv_row( $b ); }
274 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $body ), 'message' => sprintf( '%d row(s) exported.', count( $body ) ) ];
275 },
276 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
277 'meta' => $this->ro_meta(),
278 ] );
279 }
280
281 private static function csv_row( $cells ) {
282 $out = [];
283 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
284 return implode( ',', $out );
285 }
286
287 /* -------------------------- mark-entry-spam ------------------------ */
288
289 private function register_mark_entry_spam() {
290 $self = $this;
291 wp_register_ability( 'atarim/formidable-mark-entry-spam', [
292 'label' => 'Mark Formidable Entry Spam', 'category' => 'atarim',
293 'description' => 'Flag an entry as spam (is_draft=2) or clear it (is_draft=0).',
294 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
295 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
296 'execute_callback' => function( $input = [] ) {
297 global $wpdb;
298 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
299 $r = $wpdb->update( AVCF_Formidable_Helpers::items_table(), [ 'is_draft' => $is_spam ? 2 : 0 ], [ 'id' => (int) $input['entry_id'] ], [ '%d' ], [ '%d' ] );
300 if ( $r === false ) { return [ 'success' => false, 'message' => 'Update failed.' ]; }
301 return [ 'success' => true, 'message' => $is_spam ? 'Entry marked as spam.' : 'Entry unmarked from spam.' ];
302 },
303 'permission_callback' => function() use ( $self ) { return $self->can_edit_entries(); },
304 'meta' => $this->write_meta( false ),
305 ] );
306 }
307
308 /* ------------------------ update-notifications ---------------------- */
309
310 private function register_update_notifications() {
311 $self = $this;
312 wp_register_ability( 'atarim/formidable-update-notifications', [
313 'label' => 'Update Formidable Notification Recipients', 'category' => 'atarim',
314 'description' => 'Update recipient email(s) of one or more of a form\'s email-action notifications. updates: [{ notification_id (action ID), recipients: [emails] }]. Emails sanitized; invalid sets skipped and reported.',
315 '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 ],
316 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
317 'execute_callback' => function( $input = [] ) {
318 if ( ! class_exists( 'FrmFormAction' ) ) { return [ 'success' => false, 'message' => 'FrmFormAction class not available.' ]; }
319 $actions = FrmFormAction::get_action_for_form( (int) $input['form_id'], 'email' );
320 if ( ! is_array( $actions ) ) { $actions = []; }
321 $updated = []; $skipped = [];
322 foreach ( (array) $input['updates'] as $u ) {
323 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
324 $matched = null;
325 foreach ( $actions as $a ) { $aid = isset( $a->ID ) ? (string) $a->ID : ( isset( $a->id ) ? (string) $a->id : '' ); if ( $aid === $nid ) { $matched = $a; break; } }
326 if ( ! $matched ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Action id not found on this form.' ]; continue; }
327 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
328 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; continue; }
329 $settings = isset( $matched->post_content ) && is_array( $matched->post_content ) ? $matched->post_content : [];
330 $settings['email_to'] = implode( ',', $clean );
331 wp_update_post( [ 'ID' => (int) ( isset( $matched->ID ) ? $matched->ID : $matched->id ), 'post_content' => wp_slash( wp_json_encode( $settings ) ) ] );
332 $updated[] = $nid;
333 }
334 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
335 },
336 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
337 'meta' => $this->write_meta( false ),
338 ] );
339 }
340
341 /* -------------------------- duplicate-form ------------------------- */
342
343 private function register_duplicate_form() {
344 $self = $this;
345 wp_register_ability( 'atarim/formidable-duplicate-form', [
346 'label' => 'Duplicate Formidable Form', 'category' => 'atarim',
347 'description' => 'Clone a form via FrmForm::duplicate. Optional new_title.',
348 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
349 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
350 'execute_callback' => function( $input = [] ) {
351 if ( ! method_exists( 'FrmForm', 'duplicate' ) ) { return [ 'success' => false, 'message' => 'FrmForm::duplicate not available.' ]; }
352 $new_id = FrmForm::duplicate( (int) $input['form_id'], false, true );
353 if ( ! $new_id ) { return [ 'success' => false, 'message' => 'Duplicate failed.' ]; }
354 if ( isset( $input['new_title'] ) && $input['new_title'] !== '' && method_exists( 'FrmForm', 'update' ) ) {
355 FrmForm::update( (int) $new_id, [ 'name' => sanitize_text_field( $input['new_title'] ) ] );
356 }
357 return [ 'success' => true, 'new_form_id' => (int) $new_id, 'message' => sprintf( 'Duplicated as form %d.', (int) $new_id ) ];
358 },
359 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
360 'meta' => $this->write_meta( false ),
361 ] );
362 }
363 }
364