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 / forminator / class-avcf-abilities-forminator.php
atarim-visual-collaboration / third-party / forms / forminator Last commit date
class-avcf-abilities-forminator-pro.php 4 weeks ago class-avcf-abilities-forminator.php 4 weeks ago class-avcf-forminator-detector.php 1 month ago class-avcf-forminator-helpers.php 4 weeks ago
class-avcf-abilities-forminator.php
362 lines
1 <?php
2 /**
3 * Forminator — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface specialised to Forminator and namespaced
6 * atarim/forminator-*. Forms/entries go through Forminator_API; stats/spam use
7 * $wpdb on frmt_form_entry. Forminator-specific power (form create/delete,
8 * entry delete, notification management, polls/quizzes listing) lives in the
9 * -pro file. Forminator gates admin on manage_options by default, so that is
10 * the permission baseline. Built on Forminator's API; not runtime-tested.
11 *
12 * @package atarim-visual-collaboration
13 */
14
15 if ( ! defined('ABSPATH') ) {
16 exit;
17 }
18
19 class AVCF_Abilities_Forminator extends AVCF_Abilities_Base {
20
21 /** @var AVCF_Forminator_Detector */
22 private $detector;
23
24 public function __construct() {
25 $this->detector = new AVCF_Forminator_Detector();
26 }
27
28 public function register() {
29 if ( ! $this->detector->avcf_forminator_is_available() ) {
30 return;
31 }
32 $this->register_list_forms();
33 $this->register_get_form();
34 $this->register_list_entries();
35 $this->register_get_entry();
36 $this->register_get_form_stats();
37 $this->register_get_form_spam_stats();
38 $this->register_find_silent_forms();
39 $this->register_export_entries();
40 $this->register_mark_entry_spam();
41 $this->register_update_notifications();
42 $this->register_duplicate_form();
43 }
44
45 /* ------------------------------ shared ----------------------------- */
46
47 private function ro_meta() { return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ]; }
48 private function write_meta( $d = false ) { return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $d, 'idempotent' => false ] ]; }
49
50 public function can_manage() { return current_user_can( 'manage_options' ); }
51
52 /* ---------------------------- list-forms --------------------------- */
53
54 private function register_list_forms() {
55 $self = $this;
56 wp_register_ability( 'atarim/forminator-list-forms', [
57 'label' => 'List Forminator Forms', 'category' => 'atarim',
58 'description' => 'List Forminator forms: { id, title, status, entries_total, last_submission, created_at }. Optional search (name), limit, offset.',
59 '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 ],
60 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'forms' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
61 'execute_callback' => function( $input = [] ) {
62 $all = Forminator_API::get_forms( null, 1, -1 );
63 if ( ! is_array( $all ) ) { $all = []; }
64 $search = isset( $input['search'] ) ? trim( (string) $input['search'] ) : '';
65 if ( $search !== '' ) {
66 $needle = strtolower( $search );
67 $all = array_values( array_filter( $all, function( $f ) use ( $needle ) { return strpos( strtolower( AVCF_Forminator_Helpers::extract_title( $f ) ), $needle ) !== false; } ) );
68 }
69 $total = count( $all );
70 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
71 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
72 $page = array_slice( $all, $offset, $limit );
73 $out = [];
74 foreach ( $page as $f ) { if ( AVCF_Forminator_Helpers::extract_id( $f ) > 0 ) { $out[] = AVCF_Forminator_Helpers::map_form_summary( $f ); } }
75 return [ 'success' => true, 'forms' => $out, 'total' => $total, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
76 },
77 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
78 'meta' => $this->ro_meta(),
79 ] );
80 }
81
82 /* ----------------------------- get-form ---------------------------- */
83
84 private function register_get_form() {
85 $self = $this;
86 wp_register_ability( 'atarim/forminator-get-form', [
87 'label' => 'Get Forminator Form', 'category' => 'atarim',
88 'description' => 'Full Forminator form definition: fields (id=slug, type, label, required, options), email notifications, shortcode. include_raw:true adds the form settings array.',
89 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_raw' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
90 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'form' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
91 'execute_callback' => function( $input = [] ) {
92 $form = Forminator_API::get_form( (int) $input['form_id'] );
93 if ( ! $form || is_wp_error( $form ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
94 return [ 'success' => true, 'form' => AVCF_Forminator_Helpers::map_form_full( $form, ! empty( $input['include_raw'] ) ), 'message' => 'OK.' ];
95 },
96 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
97 'meta' => $this->ro_meta(),
98 ] );
99 }
100
101 /* --------------------------- list-entries -------------------------- */
102
103 private function register_list_entries() {
104 $self = $this;
105 wp_register_ability( 'atarim/forminator-list-entries', [
106 'label' => 'List Forminator Entries', 'category' => 'atarim',
107 'description' => 'List entries for a form (newest first), normalized to { id, submitted_at, status, fields[] } with labels resolved. Filters: date_from, date_to, include_spam. Paged via limit/offset.',
108 '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 ],
109 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
110 'execute_callback' => function( $input = [] ) {
111 $form_id = (int) $input['form_id'];
112 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
113 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
114 $include_spam = ! empty( $input['include_spam'] );
115 $raw = Forminator_API::get_entries( $form_id, [ 'limit' => $limit, 'offset' => $offset ] );
116 if ( is_wp_error( $raw ) ) { return [ 'success' => false, 'message' => $raw->get_error_message() ]; }
117 $form = Forminator_API::get_form( $form_id );
118 $label_map = AVCF_Forminator_Helpers::build_label_map( $form );
119 $out = []; $skipped = 0;
120 foreach ( (array) $raw as $e ) {
121 if ( AVCF_Forminator_Helpers::entry_is_spam( $e ) && ! $include_spam ) { $skipped++; continue; }
122 $at = is_object( $e ) && property_exists( $e, 'date_created_sql' ) ? (string) $e->date_created_sql : '';
123 if ( ! empty( $input['date_from'] ) && $at && strcmp( $at, (string) $input['date_from'] ) < 0 ) { continue; }
124 if ( ! empty( $input['date_to'] ) && $at && strcmp( $at, (string) $input['date_to'] ) > 0 ) { continue; }
125 $out[] = AVCF_Forminator_Helpers::normalize_entry( $e, $form_id, $label_map, false );
126 }
127 $total = AVCF_Forminator_Helpers::count_entries( $form_id, $include_spam );
128 return [ 'success' => true, 'entries' => $out, 'total' => $total, 'message' => sprintf( '%d entr(y/ies).', count( $out ) ) ];
129 },
130 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
131 'meta' => $this->ro_meta(),
132 ] );
133 }
134
135 /* ---------------------------- get-entry ---------------------------- */
136
137 private function register_get_entry() {
138 $self = $this;
139 wp_register_ability( 'atarim/forminator-get-entry', [
140 'label' => 'Get Forminator Entry', 'category' => 'atarim',
141 'description' => 'Full normalized entry by id (the owning form is resolved automatically), with field values, labels resolved, and the raw entry.',
142 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
143 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entry' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
144 'execute_callback' => function( $input = [] ) {
145 global $wpdb;
146 $entry_id = (int) $input['entry_id'];
147 $table = AVCF_Forminator_Helpers::entry_table();
148 $form_id = $wpdb->get_var( $wpdb->prepare( "SELECT form_id FROM `{$table}` WHERE entry_id = %d", $entry_id ) );
149 if ( ! $form_id ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
150 $entry = Forminator_API::get_entry( (int) $form_id, $entry_id );
151 if ( ! $entry || is_wp_error( $entry ) ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
152 $form = Forminator_API::get_form( (int) $form_id );
153 return [ 'success' => true, 'entry' => AVCF_Forminator_Helpers::normalize_entry( $entry, (int) $form_id, AVCF_Forminator_Helpers::build_label_map( $form ), true ), 'message' => 'OK.' ];
154 },
155 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
156 'meta' => $this->ro_meta(),
157 ] );
158 }
159
160 /* -------------------------- get-form-stats ------------------------- */
161
162 private function register_get_form_stats() {
163 $self = $this;
164 wp_register_ability( 'atarim/forminator-get-form-stats', [
165 'label' => 'Get Forminator Form Stats', 'category' => 'atarim',
166 'description' => 'Submission stats (non-spam): total, first/last submission, per-day counts. Optional date window.',
167 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
168 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
169 'execute_callback' => function( $input = [] ) {
170 return [ 'success' => true, 'stats' => AVCF_Forminator_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
171 },
172 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
173 'meta' => $this->ro_meta(),
174 ] );
175 }
176
177 /* ------------------------ get-form-spam-stats ---------------------- */
178
179 private function register_get_form_spam_stats() {
180 $self = $this;
181 wp_register_ability( 'atarim/forminator-get-form-spam-stats', [
182 'label' => 'Get Forminator Form Spam Stats', 'category' => 'atarim',
183 'description' => 'Spam vs non-spam entry counts and spam rate (is_spam column).',
184 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
185 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
186 'execute_callback' => function( $input = [] ) {
187 $form_id = (int) $input['form_id'];
188 $all = AVCF_Forminator_Helpers::count_entries( $form_id, true );
189 $active = AVCF_Forminator_Helpers::count_entries( $form_id, false );
190 $spam = max( 0, $all - $active );
191 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $all > 0 ? round( $spam / $all, 4 ) : 0, 'message' => sprintf( '%d spam / %d active.', $spam, $active ) ];
192 },
193 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
194 'meta' => $this->ro_meta(),
195 ] );
196 }
197
198 /* ------------------------- find-silent-forms ----------------------- */
199
200 private function register_find_silent_forms() {
201 $self = $this;
202 wp_register_ability( 'atarim/forminator-find-silent-forms', [
203 'label' => 'Find Silent Forminator Forms', 'category' => 'atarim',
204 'description' => 'Forms with no submission in the last N days (default 30). Returns each with last_submission and days_silent.',
205 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
206 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
207 'execute_callback' => function( $input = [] ) {
208 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
209 $cutoff = time() - ( $days * DAY_IN_SECONDS );
210 $all = Forminator_API::get_forms( null, 1, -1 );
211 if ( ! is_array( $all ) ) { $all = []; }
212 $silent = [];
213 foreach ( $all as $f ) {
214 $fid = AVCF_Forminator_Helpers::extract_id( $f );
215 if ( $fid <= 0 ) { continue; }
216 $last = AVCF_Forminator_Helpers::last_submission_at( $fid );
217 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
218 if ( $last_ts === false ) { $last_ts = 0; }
219 if ( $last_ts < $cutoff ) {
220 $silent[] = [ 'id' => $fid, 'title' => AVCF_Forminator_Helpers::extract_title( $f ), 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
221 }
222 }
223 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
224 },
225 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
226 'meta' => $this->ro_meta(),
227 ] );
228 }
229
230 /* -------------------------- export-entries ------------------------- */
231
232 private function register_export_entries() {
233 $self = $this;
234 wp_register_ability( 'atarim/forminator-export-entries', [
235 'label' => 'Export Forminator Entries', 'category' => 'atarim',
236 'description' => 'Export a form\'s entries as CSV text. Capped via limit (default 1000). Excludes spam unless include_spam:true.',
237 '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 ],
238 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
239 'execute_callback' => function( $input = [] ) {
240 $form_id = (int) $input['form_id'];
241 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000;
242 $raw = Forminator_API::get_entries( $form_id, [ 'limit' => $limit, 'offset' => 0 ] );
243 if ( is_wp_error( $raw ) ) { return [ 'success' => false, 'message' => $raw->get_error_message() ]; }
244 $form = Forminator_API::get_form( $form_id );
245 $label_map = AVCF_Forminator_Helpers::build_label_map( $form );
246 $header = [ 'Entry ID', 'Submitted' ]; $header_set = false; $body = [];
247 foreach ( (array) $raw as $e ) {
248 if ( AVCF_Forminator_Helpers::entry_is_spam( $e ) && empty( $input['include_spam'] ) ) { continue; }
249 $n = AVCF_Forminator_Helpers::normalize_entry( $e, $form_id, $label_map, false );
250 $line = [ (string) $n['id'], (string) $n['submitted_at'] ];
251 foreach ( $n['fields'] as $fld ) {
252 if ( ! $header_set ) { $header[] = $fld['label'] !== '' ? $fld['label'] : $fld['id']; }
253 $line[] = is_array( $fld['value'] ) ? implode( ' | ', array_map( 'strval', $fld['value'] ) ) : (string) $fld['value'];
254 }
255 $header_set = true; $body[] = $line;
256 }
257 $lines = [ self::csv_row( $header ) ];
258 foreach ( $body as $b ) { $lines[] = self::csv_row( $b ); }
259 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $body ), 'message' => sprintf( '%d row(s) exported.', count( $body ) ) ];
260 },
261 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
262 'meta' => $this->ro_meta(),
263 ] );
264 }
265
266 private static function csv_row( $cells ) {
267 $out = [];
268 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
269 return implode( ',', $out );
270 }
271
272 /* -------------------------- mark-entry-spam ------------------------ */
273
274 private function register_mark_entry_spam() {
275 $self = $this;
276 wp_register_ability( 'atarim/forminator-mark-entry-spam', [
277 'label' => 'Mark Forminator Entry Spam', 'category' => 'atarim',
278 'description' => 'Flag an entry as spam (is_spam=1) or clear it (is_spam=0).',
279 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
280 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
281 'execute_callback' => function( $input = [] ) {
282 global $wpdb;
283 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
284 $r = $wpdb->update( AVCF_Forminator_Helpers::entry_table(), [ 'is_spam' => $is_spam ? 1 : 0 ], [ 'entry_id' => (int) $input['entry_id'] ], [ '%d' ], [ '%d' ] );
285 if ( $r === false ) { return [ 'success' => false, 'message' => 'Update failed.' ]; }
286 return [ 'success' => true, 'message' => $is_spam ? 'Entry marked as spam.' : 'Entry unmarked from spam.' ];
287 },
288 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
289 'meta' => $this->write_meta( false ),
290 ] );
291 }
292
293 /* ------------------------ update-notifications ---------------------- */
294
295 private function register_update_notifications() {
296 $self = $this;
297 wp_register_ability( 'atarim/forminator-update-notifications', [
298 'label' => 'Update Forminator Notification Recipients', 'category' => 'atarim',
299 'description' => 'Update recipient email(s) of one or more of a form\'s email notifications. updates: [{ notification_id (slug or index), recipients: [emails] }]. Emails sanitized; invalid sets skipped and reported. Saved via the form model.',
300 '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 ],
301 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
302 'execute_callback' => function( $input = [] ) {
303 $form = Forminator_API::get_form( (int) $input['form_id'] );
304 if ( ! $form || is_wp_error( $form ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
305 $settings = AVCF_Forminator_Helpers::settings_array( $form );
306 $block = AVCF_Forminator_Helpers::notifications_block( $settings );
307 $key = $block[0];
308 if ( $key === null ) { return [ 'success' => false, 'message' => 'No notifications configured on this form.' ]; }
309 $updated = []; $skipped = [];
310 foreach ( (array) $input['updates'] as $u ) {
311 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
312 $matched = false;
313 foreach ( $settings[ $key ] as $idx => $n ) {
314 $cid = isset( $n['slug'] ) ? (string) $n['slug'] : (string) $idx;
315 if ( $cid !== $nid ) { continue; }
316 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
317 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; $matched = true; break; }
318 $rec = implode( ',', $clean );
319 if ( isset( $n['recipients'] ) ) { $settings[ $key ][ $idx ]['recipients'] = $rec; }
320 else { $settings[ $key ][ $idx ]['email-recipients'] = $rec; }
321 $updated[] = $nid; $matched = true; break;
322 }
323 if ( ! $matched ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Not found on this form.' ]; }
324 }
325 if ( ! empty( $updated ) && is_object( $form ) && property_exists( $form, 'settings' ) ) {
326 $form->settings = $settings;
327 if ( method_exists( $form, 'save' ) ) { $form->save(); }
328 }
329 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
330 },
331 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
332 'meta' => $this->write_meta( false ),
333 ] );
334 }
335
336 /* -------------------------- duplicate-form ------------------------- */
337
338 private function register_duplicate_form() {
339 $self = $this;
340 wp_register_ability( 'atarim/forminator-duplicate-form', [
341 'label' => 'Duplicate Forminator Form', 'category' => 'atarim',
342 'description' => 'Clone a form via Forminator_API::add_form (copies fields + settings). Optional new_title (defaults to "<title> (Copy)").',
343 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
344 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
345 'execute_callback' => function( $input = [] ) {
346 $orig = Forminator_API::get_form( (int) $input['form_id'] );
347 if ( ! $orig || is_wp_error( $orig ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
348 if ( ! method_exists( 'Forminator_API', 'add_form' ) ) { return [ 'success' => false, 'message' => 'Forminator_API::add_form unavailable.' ]; }
349 $title = isset( $input['new_title'] ) && $input['new_title'] !== '' ? sanitize_text_field( $input['new_title'] ) : ( AVCF_Forminator_Helpers::extract_title( $orig ) . ' (Copy)' );
350 $settings = AVCF_Forminator_Helpers::settings_array( $orig );
351 $settings['formName'] = $title;
352 $fields = property_exists( $orig, 'fields' ) ? $orig->fields : [];
353 $new_id = Forminator_API::add_form( $title, 'custom-forms', $fields, $settings );
354 if ( is_wp_error( $new_id ) || ! $new_id ) { return [ 'success' => false, 'message' => is_wp_error( $new_id ) ? $new_id->get_error_message() : 'Duplicate failed.' ]; }
355 return [ 'success' => true, 'new_form_id' => (int) $new_id, 'message' => sprintf( 'Duplicated as form %d.', (int) $new_id ) ];
356 },
357 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
358 'meta' => $this->write_meta( false ),
359 ] );
360 }
361 }
362