PluginProbe ʕ •ᴥ•ʔ
Atarim – Visual Feedback, Review & AI Collaboration / 5.0
Atarim – Visual Feedback, Review & AI Collaboration v5.0
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 / wpforms / class-avcf-abilities-wpforms.php
atarim-visual-collaboration / third-party / forms / wpforms Last commit date
class-avcf-abilities-wpforms-pro.php 2 weeks ago class-avcf-abilities-wpforms.php 2 weeks ago class-avcf-wpforms-detector.php 2 weeks ago class-avcf-wpforms-helpers.php 2 weeks ago
class-avcf-abilities-wpforms.php
351 lines
1 <?php
2 /**
3 * WPForms — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface specialised to WPForms and namespaced
6 * atarim/wpforms-*. Entry-dependent abilities require WPForms Pro (Lite stores
7 * no entries) and return a clear note when run on Lite. WPForms-specific power
8 * abilities (form CRUD, entry star/read/notes/delete, notifications &
9 * confirmations, providers) live in the -pro file. Permissions use WPForms'
10 * own capabilities with a manage_options fallback. Built on WPForms; not
11 * runtime-tested here.
12 *
13 * @package atarim-visual-collaboration
14 */
15
16 if ( ! defined('ABSPATH') ) {
17 exit;
18 }
19
20 class AVCF_Abilities_WPForms extends AVCF_Abilities_Base {
21
22 /** @var AVCF_WPForms_Detector */
23 private $detector;
24
25 public function __construct() {
26 $this->detector = new AVCF_WPForms_Detector();
27 }
28
29 public function register() {
30 if ( ! $this->detector->avcf_wpforms_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 is_pro() { return $this->detector->avcf_wpforms_is_pro(); }
52 public function can_view_forms() { return current_user_can( 'wpforms_edit_forms' ) || current_user_can( 'wpforms_view_entries' ) || current_user_can( 'manage_options' ); }
53 public function can_view_entries() { return current_user_can( 'wpforms_view_entries' ) || current_user_can( 'manage_options' ); }
54 public function can_edit_entries() { return current_user_can( 'wpforms_edit_entries' ) || current_user_can( 'manage_options' ); }
55 public function can_edit_forms() { return current_user_can( 'wpforms_edit_forms' ) || current_user_can( 'manage_options' ); }
56
57 private function lite_block() { return [ 'success' => false, 'message' => AVCF_WPForms_Helpers::LITE_NOTE ]; }
58
59 /* ---------------------------- list-forms --------------------------- */
60
61 private function register_list_forms() {
62 $self = $this;
63 wp_register_ability( 'atarim/wpforms-list-forms', [
64 'label' => 'List WPForms', 'category' => 'atarim',
65 'description' => 'List WPForms forms: { id, title, status, entries_total, last_submission, created_at }. entries_total/last_submission are null on WPForms Lite (no entry storage). Optional search, limit, offset.',
66 '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 ],
67 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'forms' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
68 'execute_callback' => function( $input = [] ) use ( $self ) {
69 $qa = [ 'post_type' => 'wpforms', 'post_status' => 'any', 'posts_per_page' => isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50, 'offset' => isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0, 'orderby' => 'date', 'order' => 'DESC' ];
70 if ( isset( $input['search'] ) && $input['search'] !== '' ) { $qa['s'] = (string) $input['search']; }
71 $q = new WP_Query( $qa );
72 $is_pro = $self->is_pro();
73 $out = [];
74 foreach ( $q->posts as $post ) { $out[] = AVCF_WPForms_Helpers::map_form_summary( $post, $is_pro ); }
75 return [ 'success' => true, 'forms' => $out, 'total' => (int) $q->found_posts, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
76 },
77 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
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/wpforms-get-form', [
87 'label' => 'Get WPForm', 'category' => 'atarim',
88 'description' => 'Full WPForms form definition: fields (id, type, label, required, options), notifications, confirmations, shortcode. include_raw:true adds the decoded form_data 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 = wpforms()->form->get( (int) $input['form_id'], [ 'content_only' => false ] );
93 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
94 $form_data = wpforms_decode( $form->post_content );
95 return [ 'success' => true, 'form' => AVCF_WPForms_Helpers::map_form_full( $form, $form_data, ! empty( $input['include_raw'] ) ), 'message' => 'OK.' ];
96 },
97 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
98 'meta' => $this->ro_meta(),
99 ] );
100 }
101
102 /* --------------------------- list-entries -------------------------- */
103
104 private function register_list_entries() {
105 $self = $this;
106 wp_register_ability( 'atarim/wpforms-list-entries', [
107 'label' => 'List WPForms Entries', 'category' => 'atarim',
108 'description' => 'List entries for a form (newest first), normalized to { id, submitted_at, status, starred, viewed, fields[] }. WPForms Pro only. Filters: date_from, date_to, include_spam. Paged via limit/offset.',
109 '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 ],
110 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
111 'execute_callback' => function( $input = [] ) use ( $self ) {
112 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
113 $form_id = (int) $input['form_id'];
114 $include_spam = ! empty( $input['include_spam'] );
115 $qa = [ 'form_id' => $form_id, 'number' => isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50, 'offset' => isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0 ];
116 if ( ! $include_spam ) { $qa['status'] = ''; }
117 if ( ! empty( $input['date_from'] ) ) { $qa['date_after'] = (string) $input['date_from']; }
118 if ( ! empty( $input['date_to'] ) ) { $qa['date_before'] = (string) $input['date_to']; }
119 $raw = wpforms()->entry->get_entries( $qa );
120 $out = [];
121 foreach ( (array) $raw as $r ) { $out[] = AVCF_WPForms_Helpers::normalize_entry( $r, $form_id, false ); }
122 return [ 'success' => true, 'entries' => $out, 'total' => AVCF_WPForms_Helpers::count_entries( $form_id, $include_spam, $input ), 'message' => sprintf( '%d entr(y/ies).', count( $out ) ) ];
123 },
124 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
125 'meta' => $this->ro_meta(),
126 ] );
127 }
128
129 /* ---------------------------- get-entry ---------------------------- */
130
131 private function register_get_entry() {
132 $self = $this;
133 wp_register_ability( 'atarim/wpforms-get-entry', [
134 'label' => 'Get WPForms Entry', 'category' => 'atarim',
135 'description' => 'Full normalized entry by id, including all field values and the raw entry. WPForms Pro only.',
136 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
137 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entry' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
138 'execute_callback' => function( $input = [] ) use ( $self ) {
139 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
140 $entry = wpforms()->entry->get( (int) $input['entry_id'] );
141 if ( ! $entry ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
142 $form_id = isset( $entry->form_id ) ? (int) $entry->form_id : 0;
143 return [ 'success' => true, 'entry' => AVCF_WPForms_Helpers::normalize_entry( $entry, $form_id, true ), 'message' => 'OK.' ];
144 },
145 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
146 'meta' => $this->ro_meta(),
147 ] );
148 }
149
150 /* -------------------------- get-form-stats ------------------------- */
151
152 private function register_get_form_stats() {
153 $self = $this;
154 wp_register_ability( 'atarim/wpforms-get-form-stats', [
155 'label' => 'Get WPForms Form Stats', 'category' => 'atarim',
156 'description' => 'Submission stats: total, first/last submission, per-day counts. WPForms Pro only. Optional date window.',
157 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
158 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
159 'execute_callback' => function( $input = [] ) use ( $self ) {
160 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
161 return [ 'success' => true, 'stats' => AVCF_WPForms_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
162 },
163 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
164 'meta' => $this->ro_meta(),
165 ] );
166 }
167
168 /* ------------------------ get-form-spam-stats ---------------------- */
169
170 private function register_get_form_spam_stats() {
171 $self = $this;
172 wp_register_ability( 'atarim/wpforms-get-form-spam-stats', [
173 'label' => 'Get WPForms Form Spam Stats', 'category' => 'atarim',
174 'description' => 'Spam vs non-spam entry counts and spam rate for a form. WPForms Pro only.',
175 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
176 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
177 'execute_callback' => function( $input = [] ) use ( $self ) {
178 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
179 $form_id = (int) $input['form_id'];
180 $all = AVCF_WPForms_Helpers::count_entries( $form_id, true );
181 $active = AVCF_WPForms_Helpers::count_entries( $form_id, false );
182 $spam = max( 0, $all - $active );
183 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $all > 0 ? round( $spam / $all, 4 ) : 0, 'message' => sprintf( '%d spam / %d active.', $spam, $active ) ];
184 },
185 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
186 'meta' => $this->ro_meta(),
187 ] );
188 }
189
190 /* ------------------------- find-silent-forms ----------------------- */
191
192 private function register_find_silent_forms() {
193 $self = $this;
194 wp_register_ability( 'atarim/wpforms-find-silent-forms', [
195 'label' => 'Find Silent WPForms', 'category' => 'atarim',
196 'description' => 'Forms with no submission in the last N days (default 30). WPForms Pro only (needs entry data). Returns each with last_submission and days_silent.',
197 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
198 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
199 'execute_callback' => function( $input = [] ) use ( $self ) {
200 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
201 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
202 $cutoff = time() - ( $days * DAY_IN_SECONDS );
203 $q = new WP_Query( [ 'post_type' => 'wpforms', 'post_status' => 'any', 'posts_per_page' => 500, 'fields' => 'ids' ] );
204 $silent = [];
205 foreach ( $q->posts as $fid ) {
206 $fid = (int) $fid;
207 $last = AVCF_WPForms_Helpers::last_submission_at( $fid );
208 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
209 if ( $last_ts === false ) { $last_ts = 0; }
210 if ( $last_ts < $cutoff ) {
211 $silent[] = [ 'id' => $fid, 'title' => get_the_title( $fid ), 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
212 }
213 }
214 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
215 },
216 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
217 'meta' => $this->ro_meta(),
218 ] );
219 }
220
221 /* -------------------------- export-entries ------------------------- */
222
223 private function register_export_entries() {
224 $self = $this;
225 wp_register_ability( 'atarim/wpforms-export-entries', [
226 'label' => 'Export WPForms Entries', 'category' => 'atarim',
227 'description' => 'Export a form\'s entries as CSV text. WPForms Pro only. Capped via limit (default 1000). Excludes spam unless include_spam:true.',
228 '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 ],
229 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
230 'execute_callback' => function( $input = [] ) use ( $self ) {
231 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
232 $form_id = (int) $input['form_id'];
233 $qa = [ 'form_id' => $form_id, 'number' => isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000, 'offset' => 0 ];
234 if ( empty( $input['include_spam'] ) ) { $qa['status'] = ''; }
235 $raw = wpforms()->entry->get_entries( $qa );
236 $rows = []; $header_set = false; $header = [ 'Entry ID', 'Submitted' ];
237 $body = [];
238 foreach ( (array) $raw as $e ) {
239 $n = AVCF_WPForms_Helpers::normalize_entry( $e, $form_id, false );
240 $line = [ (string) $n['id'], (string) $n['submitted_at'] ];
241 foreach ( $n['fields'] as $fld ) {
242 if ( ! $header_set ) { $header[] = $fld['label'] !== '' ? $fld['label'] : ( 'Field ' . $fld['id'] ); }
243 $val = is_array( $fld['value'] ) ? implode( ' | ', $fld['value'] ) : (string) $fld['value'];
244 $line[] = $val;
245 }
246 $header_set = true;
247 $body[] = $line;
248 }
249 $lines = [ self::csv_row( $header ) ];
250 foreach ( $body as $b ) { $lines[] = self::csv_row( $b ); }
251 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $body ), 'message' => sprintf( '%d row(s) exported.', count( $body ) ) ];
252 },
253 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
254 'meta' => $this->ro_meta(),
255 ] );
256 }
257
258 private static function csv_row( $cells ) {
259 $out = [];
260 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
261 return implode( ',', $out );
262 }
263
264 /* -------------------------- mark-entry-spam ------------------------ */
265
266 private function register_mark_entry_spam() {
267 $self = $this;
268 wp_register_ability( 'atarim/wpforms-mark-entry-spam', [
269 'label' => 'Mark WPForms Entry Spam', 'category' => 'atarim',
270 'description' => 'Flag an entry as spam (or clear with is_spam:false). WPForms Pro only.',
271 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
272 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
273 'execute_callback' => function( $input = [] ) use ( $self ) {
274 if ( ! $self->is_pro() ) { return $self->lite_block_public(); }
275 $entry_id = (int) $input['entry_id'];
276 $entry = wpforms()->entry->get( $entry_id );
277 if ( ! $entry ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
278 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
279 $r = wpforms()->entry->update( $entry_id, [ 'status' => $is_spam ? 'spam' : '' ], '', 'edit', [ 'cap' => 'edit_entries_form_single' ] );
280 if ( ! $r ) { return [ 'success' => false, 'message' => 'Update failed.' ]; }
281 return [ 'success' => true, 'message' => $is_spam ? 'Entry marked as spam.' : 'Entry unmarked from spam.' ];
282 },
283 'permission_callback' => function() use ( $self ) { return $self->can_edit_entries(); },
284 'meta' => $this->write_meta( false ),
285 ] );
286 }
287
288 /* ------------------------ update-notifications ---------------------- */
289
290 private function register_update_notifications() {
291 $self = $this;
292 wp_register_ability( 'atarim/wpforms-update-notifications', [
293 'label' => 'Update WPForms Notification Recipients', 'category' => 'atarim',
294 '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.',
295 '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 ],
296 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
297 'execute_callback' => function( $input = [] ) {
298 $form = wpforms()->form->get( (int) $input['form_id'], [ 'content_only' => false ] );
299 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
300 $form_data = wpforms_decode( $form->post_content );
301 if ( ! is_array( $form_data ) ) { $form_data = []; }
302 $updated = []; $skipped = [];
303 foreach ( (array) $input['updates'] as $u ) {
304 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
305 if ( ! isset( $form_data['settings']['notifications'][ $nid ] ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Not found on this form.' ]; continue; }
306 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
307 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; continue; }
308 $form_data['settings']['notifications'][ $nid ]['email'] = implode( ',', $clean );
309 $updated[] = $nid;
310 }
311 if ( ! empty( $updated ) ) { wp_update_post( [ 'ID' => (int) $input['form_id'], 'post_content' => wpforms_encode( $form_data ) ] ); }
312 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
313 },
314 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
315 'meta' => $this->write_meta( false ),
316 ] );
317 }
318
319 /* -------------------------- duplicate-form ------------------------- */
320
321 private function register_duplicate_form() {
322 $self = $this;
323 wp_register_ability( 'atarim/wpforms-duplicate-form', [
324 'label' => 'Duplicate WPForm', 'category' => 'atarim',
325 'description' => 'Clone a form via WPForms\' built-in duplicate. Optional new_title.',
326 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
327 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
328 'execute_callback' => function( $input = [] ) {
329 $form_id = (int) $input['form_id'];
330 if ( ! wpforms()->form->get( $form_id, [ 'content_only' => false ] ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
331 if ( ! method_exists( wpforms()->form, 'duplicate' ) ) { return [ 'success' => false, 'message' => 'WPForms duplicate method not available.' ]; }
332 $new_id = wpforms()->form->duplicate( $form_id );
333 if ( is_wp_error( $new_id ) || ! $new_id ) { return [ 'success' => false, 'message' => 'Duplicate failed.' ]; }
334 if ( isset( $input['new_title'] ) && $input['new_title'] !== '' ) {
335 $nf = wpforms()->form->get( $new_id, [ 'content_only' => false ] );
336 if ( $nf ) {
337 $nd = wpforms_decode( $nf->post_content );
338 if ( is_array( $nd ) ) { $nd['settings']['form_title'] = sanitize_text_field( $input['new_title'] ); wp_update_post( [ 'ID' => $new_id, 'post_title' => sanitize_text_field( $input['new_title'] ), 'post_content' => wpforms_encode( $nd ) ] ); }
339 }
340 }
341 return [ 'success' => true, 'new_form_id' => (int) $new_id, 'message' => sprintf( 'Duplicated as form %d.', (int) $new_id ) ];
342 },
343 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
344 'meta' => $this->write_meta( false ),
345 ] );
346 }
347
348 /** public wrapper so closures can emit the Lite note */
349 public function lite_block_public() { return $this->lite_block(); }
350 }
351