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 / ninja-forms / class-avcf-abilities-ninja.php
atarim-visual-collaboration / third-party / forms / ninja-forms Last commit date
class-avcf-abilities-ninja-pro.php 1 month ago class-avcf-abilities-ninja.php 1 month ago class-avcf-ninja-detector.php 1 month ago class-avcf-ninja-helpers.php 1 month ago
class-avcf-abilities-ninja.php
349 lines
1 <?php
2 /**
3 * Ninja Forms — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface specialised to Ninja Forms and namespaced
6 * atarim/ninja-*. Forms/fields/actions/subs go through the Ninja_Forms() model
7 * API; submission counts/stats use $wpdb over posts/postmeta. Ninja has no
8 * native spam flag — it uses the trash status as the spam-equivalent.
9 * Ninja-specific power (form delete, field/action reads, submission delete,
10 * notification toggle/save) lives in the -pro file. Permission baseline is
11 * manage_options. Built on Ninja's API; not runtime-tested here.
12 *
13 * @package atarim-visual-collaboration
14 */
15
16 if ( ! defined('ABSPATH') ) {
17 exit;
18 }
19
20 class AVCF_Abilities_Ninja extends AVCF_Abilities_Base {
21
22 /** @var AVCF_Ninja_Detector */
23 private $detector;
24
25 public function __construct() {
26 $this->detector = new AVCF_Ninja_Detector();
27 }
28
29 public function register() {
30 if ( ! $this->detector->avcf_ninja_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_manage() { return current_user_can( 'manage_options' ); }
52
53 public function forms_table() { global $wpdb; return $wpdb->prefix . 'nf3_forms'; }
54
55 /* ---------------------------- list-forms --------------------------- */
56
57 private function register_list_forms() {
58 $self = $this;
59 wp_register_ability( 'atarim/ninja-list-forms', [
60 'label' => 'List Ninja Forms', 'category' => 'atarim',
61 'description' => 'List Ninja Forms: { id, title, status, entries_total, last_submission, created_at }. Optional search (title), limit, offset.',
62 '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 ],
63 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'forms' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
64 'execute_callback' => function( $input = [] ) use ( $self ) {
65 global $wpdb;
66 $table = $self->forms_table();
67 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
68 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
69 $search = isset( $input['search'] ) ? trim( (string) $input['search'] ) : '';
70 if ( $search !== '' ) {
71 $like = '%' . $wpdb->esc_like( $search ) . '%';
72 $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE title LIKE %s", $like ) );
73 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, created_at FROM `{$table}` WHERE title LIKE %s ORDER BY id DESC LIMIT %d OFFSET %d", $like, $limit, $offset ) );
74 } else {
75 $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
76 $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, created_at FROM `{$table}` ORDER BY id DESC LIMIT %d OFFSET %d", $limit, $offset ) );
77 }
78 $out = [];
79 foreach ( (array) $rows as $r ) { $out[] = AVCF_Ninja_Helpers::map_form_summary( $r ); }
80 return [ 'success' => true, 'forms' => $out, 'total' => $total, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
81 },
82 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
83 'meta' => $this->ro_meta(),
84 ] );
85 }
86
87 /* ----------------------------- get-form ---------------------------- */
88
89 private function register_get_form() {
90 $self = $this;
91 wp_register_ability( 'atarim/ninja-get-form', [
92 'label' => 'Get Ninja Form', 'category' => 'atarim',
93 'description' => 'Full Ninja form definition: fields (id, key, type, label, required, options), email-action notifications, shortcode. include_raw:true adds the form settings.',
94 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_raw' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
95 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'form' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
96 'execute_callback' => function( $input = [] ) {
97 $form = AVCF_Ninja_Helpers::map_form_full( (int) $input['form_id'], ! empty( $input['include_raw'] ) );
98 if ( $form === null ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
99 return [ 'success' => true, 'form' => $form, 'message' => 'OK.' ];
100 },
101 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
102 'meta' => $this->ro_meta(),
103 ] );
104 }
105
106 /* --------------------------- list-entries -------------------------- */
107
108 private function register_list_entries() {
109 $self = $this;
110 wp_register_ability( 'atarim/ninja-list-entries', [
111 'label' => 'List Ninja Entries', 'category' => 'atarim',
112 'description' => 'List submissions for a form (newest first), normalized to { id, submitted_at, status, fields[] } with labels resolved. include_spam includes trashed submissions. Filters: date_from, date_to. Paged via limit/offset.',
113 '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 ],
114 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
115 'execute_callback' => function( $input = [] ) {
116 $form_id = (int) $input['form_id'];
117 $include_spam = ! empty( $input['include_spam'] );
118 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
119 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
120 $ids = AVCF_Ninja_Helpers::sub_ids( $form_id, $include_spam, $input, $limit, $offset );
121 $total = AVCF_Ninja_Helpers::count_submissions( $form_id, $include_spam, $input );
122 $label_map = AVCF_Ninja_Helpers::build_label_map( $form_id );
123 $out = [];
124 foreach ( (array) $ids as $sid ) {
125 $sub = Ninja_Forms()->form( $form_id )->sub( (int) $sid )->get();
126 $out[] = AVCF_Ninja_Helpers::normalize_sub( $sub, $form_id, $label_map, (int) $sid, false );
127 }
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/ninja-get-entry', [
140 'label' => 'Get Ninja Entry', 'category' => 'atarim',
141 'description' => 'Full normalized submission by id (the owning form is resolved from the _form_id meta), with field values and labels resolved.',
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 $entry_id = (int) $input['entry_id'];
146 $post = get_post( $entry_id );
147 if ( ! $post || $post->post_type !== 'nf_sub' ) { return [ 'success' => false, 'message' => 'Submission not found.' ]; }
148 $form_id = (int) get_post_meta( $entry_id, '_form_id', true );
149 if ( ! $form_id ) { return [ 'success' => false, 'message' => 'Submission has no linked form.' ]; }
150 $sub = Ninja_Forms()->form( $form_id )->sub( $entry_id )->get();
151 return [ 'success' => true, 'entry' => AVCF_Ninja_Helpers::normalize_sub( $sub, $form_id, AVCF_Ninja_Helpers::build_label_map( $form_id ), $entry_id, true ), 'message' => 'OK.' ];
152 },
153 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
154 'meta' => $this->ro_meta(),
155 ] );
156 }
157
158 /* -------------------------- get-form-stats ------------------------- */
159
160 private function register_get_form_stats() {
161 $self = $this;
162 wp_register_ability( 'atarim/ninja-get-form-stats', [
163 'label' => 'Get Ninja Form Stats', 'category' => 'atarim',
164 'description' => 'Submission stats (published, non-trashed): total, first/last submission, per-day counts. Optional date window.',
165 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
166 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
167 'execute_callback' => function( $input = [] ) {
168 return [ 'success' => true, 'stats' => AVCF_Ninja_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
169 },
170 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
171 'meta' => $this->ro_meta(),
172 ] );
173 }
174
175 /* ------------------------ get-form-spam-stats ---------------------- */
176
177 private function register_get_form_spam_stats() {
178 $self = $this;
179 wp_register_ability( 'atarim/ninja-get-form-spam-stats', [
180 'label' => 'Get Ninja Form Spam Stats', 'category' => 'atarim',
181 'description' => 'Trashed (spam-equivalent) vs published submission counts and rate. Ninja has no native spam flag; trashed submissions are treated as spam.',
182 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
183 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
184 'execute_callback' => function( $input = [] ) {
185 $form_id = (int) $input['form_id'];
186 $all = AVCF_Ninja_Helpers::count_submissions( $form_id, true );
187 $active = AVCF_Ninja_Helpers::count_submissions( $form_id, false );
188 $spam = max( 0, $all - $active );
189 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $all > 0 ? round( $spam / $all, 4 ) : 0, 'message' => sprintf( '%d trashed / %d active.', $spam, $active ) ];
190 },
191 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
192 'meta' => $this->ro_meta(),
193 ] );
194 }
195
196 /* ------------------------- find-silent-forms ----------------------- */
197
198 private function register_find_silent_forms() {
199 $self = $this;
200 wp_register_ability( 'atarim/ninja-find-silent-forms', [
201 'label' => 'Find Silent Ninja Forms', 'category' => 'atarim',
202 'description' => 'Forms with no submission in the last N days (default 30). Returns each with last_submission and days_silent.',
203 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
204 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
205 'execute_callback' => function( $input = [] ) use ( $self ) {
206 global $wpdb;
207 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
208 $cutoff = time() - ( $days * DAY_IN_SECONDS );
209 $table = $self->forms_table();
210 $rows = $wpdb->get_results( "SELECT id, title FROM `{$table}` ORDER BY id DESC LIMIT 500" );
211 $silent = [];
212 foreach ( (array) $rows as $r ) {
213 $fid = (int) $r->id;
214 $last = AVCF_Ninja_Helpers::last_submission_at( $fid );
215 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
216 if ( $last_ts === false ) { $last_ts = 0; }
217 if ( $last_ts < $cutoff ) {
218 $silent[] = [ 'id' => $fid, 'title' => (string) $r->title, 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
219 }
220 }
221 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
222 },
223 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
224 'meta' => $this->ro_meta(),
225 ] );
226 }
227
228 /* -------------------------- export-entries ------------------------- */
229
230 private function register_export_entries() {
231 $self = $this;
232 wp_register_ability( 'atarim/ninja-export-entries', [
233 'label' => 'Export Ninja Entries', 'category' => 'atarim',
234 'description' => 'Export a form\'s submissions as CSV text. Capped via limit (default 1000). Excludes trashed unless include_spam:true.',
235 '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 ],
236 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
237 'execute_callback' => function( $input = [] ) {
238 $form_id = (int) $input['form_id'];
239 $include_spam = ! empty( $input['include_spam'] );
240 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000;
241 $ids = AVCF_Ninja_Helpers::sub_ids( $form_id, $include_spam, [], $limit, 0 );
242 $label_map = AVCF_Ninja_Helpers::build_label_map( $form_id );
243 $header = [ 'Entry ID', 'Submitted' ]; $header_set = false; $body = [];
244 foreach ( (array) $ids as $sid ) {
245 $sub = Ninja_Forms()->form( $form_id )->sub( (int) $sid )->get();
246 $n = AVCF_Ninja_Helpers::normalize_sub( $sub, $form_id, $label_map, (int) $sid, false );
247 $line = [ (string) $n['id'], (string) $n['submitted_at'] ];
248 foreach ( $n['fields'] as $fld ) {
249 if ( ! $header_set ) { $header[] = $fld['label'] !== '' ? $fld['label'] : $fld['id']; }
250 $line[] = is_array( $fld['value'] ) ? implode( ' | ', array_map( 'strval', $fld['value'] ) ) : (string) $fld['value'];
251 }
252 $header_set = true; $body[] = $line;
253 }
254 $lines = [ self::csv_row( $header ) ];
255 foreach ( $body as $b ) { $lines[] = self::csv_row( $b ); }
256 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $body ), 'message' => sprintf( '%d row(s) exported.', count( $body ) ) ];
257 },
258 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
259 'meta' => $this->ro_meta(),
260 ] );
261 }
262
263 private static function csv_row( $cells ) {
264 $out = [];
265 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
266 return implode( ',', $out );
267 }
268
269 /* -------------------------- mark-entry-spam ------------------------ */
270
271 private function register_mark_entry_spam() {
272 $self = $this;
273 wp_register_ability( 'atarim/ninja-mark-entry-spam', [
274 'label' => 'Mark Ninja Entry Spam', 'category' => 'atarim',
275 'description' => 'Trash a submission (Ninja\'s spam-equivalent) or restore it (is_spam:false → publish).',
276 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
277 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
278 'execute_callback' => function( $input = [] ) {
279 $entry_id = (int) $input['entry_id'];
280 $post = get_post( $entry_id );
281 if ( ! $post || $post->post_type !== 'nf_sub' ) { return [ 'success' => false, 'message' => 'Submission not found.' ]; }
282 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
283 $r = wp_update_post( [ 'ID' => $entry_id, 'post_status' => $is_spam ? 'trash' : 'publish' ], true );
284 if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => $r->get_error_message() ]; }
285 return [ 'success' => true, 'message' => $is_spam ? 'Submission trashed (spam-equivalent).' : 'Submission restored.' ];
286 },
287 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
288 'meta' => $this->write_meta( false ),
289 ] );
290 }
291
292 /* ------------------------ update-notifications ---------------------- */
293
294 private function register_update_notifications() {
295 $self = $this;
296 wp_register_ability( 'atarim/ninja-update-notifications', [
297 'label' => 'Update Ninja Notification Recipients', 'category' => 'atarim',
298 'description' => 'Update recipient email(s) of one or more email actions. updates: [{ notification_id (action id), recipients: [emails] }]. Emails sanitized; non-email or missing actions skipped and reported.',
299 '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 ],
300 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
301 'execute_callback' => function( $input = [] ) {
302 $form_id = (int) $input['form_id'];
303 $actions = Ninja_Forms()->form( $form_id )->get_actions();
304 if ( empty( $actions ) ) { return [ 'success' => false, 'message' => 'No actions configured on this form.' ]; }
305 $updated = []; $skipped = [];
306 foreach ( (array) $input['updates'] as $u ) {
307 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
308 $action = isset( $actions[ $nid ] ) ? $actions[ $nid ] : null;
309 if ( ! $action ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Action not found.' ]; continue; }
310 $settings = method_exists( $action, 'get_settings' ) ? $action->get_settings() : [];
311 if ( ! isset( $settings['type'] ) || $settings['type'] !== 'email' ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Not an email action.' ]; continue; }
312 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
313 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; continue; }
314 if ( method_exists( $action, 'update_setting' ) ) { $action->update_setting( 'to', implode( ',', $clean ) ); }
315 if ( method_exists( $action, 'save' ) ) { $action->save(); }
316 $updated[] = $nid;
317 }
318 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
319 },
320 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
321 'meta' => $this->write_meta( false ),
322 ] );
323 }
324
325 /* -------------------------- duplicate-form ------------------------- */
326
327 private function register_duplicate_form() {
328 $self = $this;
329 wp_register_ability( 'atarim/ninja-duplicate-form', [
330 'label' => 'Duplicate Ninja Form', 'category' => 'atarim',
331 'description' => 'Clone a form via NF_Database_Models_Form::duplicate. Optional new_title.',
332 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
333 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
334 'execute_callback' => function( $input = [] ) {
335 if ( ! class_exists( 'NF_Database_Models_Form' ) || ! method_exists( 'NF_Database_Models_Form', 'duplicate' ) ) { return [ 'success' => false, 'message' => 'Ninja duplicate method unavailable.' ]; }
336 $new_id = NF_Database_Models_Form::duplicate( (int) $input['form_id'] );
337 if ( ! $new_id ) { return [ 'success' => false, 'message' => 'Duplicate failed.' ]; }
338 if ( isset( $input['new_title'] ) && $input['new_title'] !== '' ) {
339 $nf = Ninja_Forms()->form( $new_id )->get();
340 if ( $nf && method_exists( $nf, 'update_setting' ) ) { $nf->update_setting( 'title', sanitize_text_field( $input['new_title'] ) ); if ( method_exists( $nf, 'save' ) ) { $nf->save(); } }
341 }
342 return [ 'success' => true, 'new_form_id' => (int) $new_id, 'message' => sprintf( 'Duplicated as form %d.', (int) $new_id ) ];
343 },
344 'permission_callback' => function() use ( $self ) { return $self->can_manage(); },
345 'meta' => $this->write_meta( false ),
346 ] );
347 }
348 }
349