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 / gravity-forms / class-avcf-abilities-gravity.php
atarim-visual-collaboration / third-party / forms / gravity-forms Last commit date
class-avcf-abilities-gravity-pro.php 1 month ago class-avcf-abilities-gravity.php 1 month ago class-avcf-gravity-detector.php 1 month ago class-avcf-gravity-helpers.php 1 month ago
class-avcf-abilities-gravity.php
350 lines
1 <?php
2 /**
3 * Gravity Forms — core MCP abilities (standalone cluster).
4 *
5 * The common forms surface (list/get forms, list/get entries, stats, spam,
6 * silent forms, export, mark-spam, notifications, duplicate), specialised to
7 * Gravity and namespaced atarim/gravity-*. GF-specific power abilities (form &
8 * entry CRUD, notes, notifications/confirmations/feeds detail) live in the -pro
9 * file. Permissions use Gravity's own capabilities with a manage_options
10 * fallback. Built on GFAPI; not runtime-tested here.
11 *
12 * @package atarim-visual-collaboration
13 */
14
15 if ( ! defined('ABSPATH') ) {
16 exit;
17 }
18
19 class AVCF_Abilities_Gravity extends AVCF_Abilities_Base {
20
21 /** @var AVCF_Gravity_Detector */
22 private $detector;
23
24 public function __construct() {
25 $this->detector = new AVCF_Gravity_Detector();
26 }
27
28 public function register() {
29 if ( ! $this->detector->avcf_gravity_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_view_forms() { return current_user_can( 'gravityforms_edit_forms' ) || current_user_can( 'gravityforms_view_entries' ) || current_user_can( 'manage_options' ); }
51 public function can_view_entries() { return current_user_can( 'gravityforms_view_entries' ) || current_user_can( 'manage_options' ); }
52 public function can_edit_entries() { return current_user_can( 'gravityforms_edit_entries' ) || current_user_can( 'manage_options' ); }
53 public function can_edit_forms() { return current_user_can( 'gravityforms_edit_forms' ) || current_user_can( 'manage_options' ); }
54
55 /* ---------------------------- list-forms --------------------------- */
56
57 private function register_list_forms() {
58 $self = $this;
59 wp_register_ability( 'atarim/gravity-list-forms', [
60 'label' => 'List Gravity Forms', 'category' => 'atarim',
61 'description' => 'List Gravity 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 = [] ) {
65 $all = GFAPI::get_forms( true, false );
66 if ( ! is_array( $all ) ) { $all = []; }
67 $search = isset( $input['search'] ) ? trim( (string) $input['search'] ) : '';
68 if ( $search !== '' ) {
69 $needle = strtolower( $search );
70 $all = array_values( array_filter( $all, function( $f ) use ( $needle ) { return isset( $f['title'] ) && strpos( strtolower( (string) $f['title'] ), $needle ) !== false; } ) );
71 }
72 $total = count( $all );
73 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
74 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
75 $out = [];
76 foreach ( array_slice( $all, $offset, $limit ) as $f ) { $out[] = AVCF_Gravity_Helpers::map_form_summary( $f ); }
77 return [ 'success' => true, 'forms' => $out, 'total' => $total, 'message' => sprintf( '%d form(s).', count( $out ) ) ];
78 },
79 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
80 'meta' => $this->ro_meta(),
81 ] );
82 }
83
84 /* ----------------------------- get-form ---------------------------- */
85
86 private function register_get_form() {
87 $self = $this;
88 wp_register_ability( 'atarim/gravity-get-form', [
89 'label' => 'Get Gravity Form', 'category' => 'atarim',
90 'description' => 'Full Gravity form definition: fields (id, type, label, required, options, has_conditional), notifications, confirmations, shortcode. include_raw:true adds the raw GF form array.',
91 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'include_raw' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
92 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'form' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
93 'execute_callback' => function( $input = [] ) {
94 $form = GFAPI::get_form( (int) $input['form_id'] );
95 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
96 return [ 'success' => true, 'form' => AVCF_Gravity_Helpers::map_form_full( $form, ! empty( $input['include_raw'] ) ), 'message' => 'OK.' ];
97 },
98 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
99 'meta' => $this->ro_meta(),
100 ] );
101 }
102
103 /* --------------------------- list-entries -------------------------- */
104
105 private function register_list_entries() {
106 $self = $this;
107 wp_register_ability( 'atarim/gravity-list-entries', [
108 'label' => 'List Gravity Entries', 'category' => 'atarim',
109 'description' => 'List entries for a form (newest first), normalized to { id, submitted_at, status, payment_status, fields[] }. Filters: date_from, date_to (Y-m-d), include_spam. Paged via limit/offset.',
110 '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 ],
111 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entries' => [ 'type' => 'array' ], 'total' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
112 'execute_callback' => function( $input = [] ) {
113 $form_id = (int) $input['form_id'];
114 $search = empty( $input['include_spam'] ) ? [ 'status' => 'active' ] : [];
115 if ( ! empty( $input['date_from'] ) ) { $search['start_date'] = (string) $input['date_from']; }
116 if ( ! empty( $input['date_to'] ) ) { $search['end_date'] = (string) $input['date_to']; }
117 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 50;
118 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
119 $raw = GFAPI::get_entries( $form_id, $search, [ 'key' => 'date_created', 'direction' => 'DESC' ], [ 'offset' => $offset, 'page_size' => $limit ] );
120 if ( is_wp_error( $raw ) ) { return [ 'success' => false, 'message' => $raw->get_error_message() ]; }
121 $total = GFAPI::count_entries( $form_id, $search );
122 $form = GFAPI::get_form( $form_id );
123 $out = [];
124 foreach ( (array) $raw as $r ) { $out[] = AVCF_Gravity_Helpers::normalize_entry( $r, $form_id, $form, false ); }
125 return [ 'success' => true, 'entries' => $out, 'total' => (int) $total, 'message' => sprintf( '%d entr(y/ies).', count( $out ) ) ];
126 },
127 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
128 'meta' => $this->ro_meta(),
129 ] );
130 }
131
132 /* ---------------------------- get-entry ---------------------------- */
133
134 private function register_get_entry() {
135 $self = $this;
136 wp_register_ability( 'atarim/gravity-get-entry', [
137 'label' => 'Get Gravity Entry', 'category' => 'atarim',
138 'description' => 'Full normalized entry by id, including all field values and the raw GF entry array.',
139 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
140 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'entry' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
141 'execute_callback' => function( $input = [] ) {
142 $entry = GFAPI::get_entry( (int) $input['entry_id'] );
143 if ( is_wp_error( $entry ) || ! $entry ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
144 $form_id = isset( $entry['form_id'] ) ? (int) $entry['form_id'] : 0;
145 $form = $form_id > 0 ? GFAPI::get_form( $form_id ) : null;
146 return [ 'success' => true, 'entry' => AVCF_Gravity_Helpers::normalize_entry( $entry, $form_id, $form, true ), 'message' => 'OK.' ];
147 },
148 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
149 'meta' => $this->ro_meta(),
150 ] );
151 }
152
153 /* -------------------------- get-form-stats ------------------------- */
154
155 private function register_get_form_stats() {
156 $self = $this;
157 wp_register_ability( 'atarim/gravity-get-form-stats', [
158 'label' => 'Get Gravity Form Stats', 'category' => 'atarim',
159 'description' => 'Submission stats for a form: total, first/last submission dates, and per-day counts. Optional date_from/date_to window.',
160 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'date_from' => [ 'type' => 'string' ], 'date_to' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
161 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stats' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
162 'execute_callback' => function( $input = [] ) {
163 if ( ! GFAPI::get_form( (int) $input['form_id'] ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
164 return [ 'success' => true, 'stats' => AVCF_Gravity_Helpers::stats( (int) $input['form_id'], $input ), 'message' => 'OK.' ];
165 },
166 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
167 'meta' => $this->ro_meta(),
168 ] );
169 }
170
171 /* ------------------------ get-form-spam-stats ---------------------- */
172
173 private function register_get_form_spam_stats() {
174 $self = $this;
175 wp_register_ability( 'atarim/gravity-get-form-spam-stats', [
176 'label' => 'Get Gravity Form Spam Stats', 'category' => 'atarim',
177 'description' => 'Spam vs active entry counts for a form, plus the spam rate.',
178 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
179 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'spam' => [ 'type' => 'integer' ], 'active' => [ 'type' => 'integer' ], 'spam_rate' => [ 'type' => 'number' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
180 'execute_callback' => function( $input = [] ) {
181 $form_id = (int) $input['form_id'];
182 if ( ! GFAPI::get_form( $form_id ) ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
183 $spam = (int) GFAPI::count_entries( $form_id, [ 'status' => 'spam' ] );
184 $active = (int) GFAPI::count_entries( $form_id, [ 'status' => 'active' ] );
185 $denom = $spam + $active;
186 return [ 'success' => true, 'spam' => $spam, 'active' => $active, 'spam_rate' => $denom > 0 ? round( $spam / $denom, 4 ) : 0, 'message' => sprintf( '%d spam / %d active.', $spam, $active ) ];
187 },
188 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
189 'meta' => $this->ro_meta(),
190 ] );
191 }
192
193 /* ------------------------- find-silent-forms ----------------------- */
194
195 private function register_find_silent_forms() {
196 $self = $this;
197 wp_register_ability( 'atarim/gravity-find-silent-forms', [
198 'label' => 'Find Silent Gravity Forms', 'category' => 'atarim',
199 'description' => 'Forms with no submission in the last N days (default 30) — useful for spotting broken or abandoned forms. Returns each form with its last_submission and days_silent (null last_submission = never received an entry).',
200 'input_schema' => [ 'type' => 'object', 'properties' => [ 'silence_days' => [ 'type' => 'integer', 'minimum' => 1, 'default' => 30 ] ], 'additionalProperties' => false ],
201 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'silent_forms' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
202 'execute_callback' => function( $input = [] ) {
203 $days = isset( $input['silence_days'] ) ? max( 1, (int) $input['silence_days'] ) : 30;
204 $cutoff = time() - ( $days * DAY_IN_SECONDS );
205 $all = GFAPI::get_forms( true, false );
206 if ( ! is_array( $all ) ) { $all = []; }
207 $silent = [];
208 foreach ( $all as $f ) {
209 $fid = isset( $f['id'] ) ? (int) $f['id'] : 0;
210 if ( $fid <= 0 ) { continue; }
211 $last = AVCF_Gravity_Helpers::last_submission_at( $fid );
212 $last_ts = $last ? strtotime( $last . ' UTC' ) : 0;
213 if ( $last_ts === false ) { $last_ts = 0; }
214 if ( $last_ts < $cutoff ) {
215 $silent[] = [ 'id' => $fid, 'title' => isset( $f['title'] ) ? (string) $f['title'] : '', 'last_submission' => $last, 'days_silent' => $last_ts > 0 ? (int) floor( ( time() - $last_ts ) / DAY_IN_SECONDS ) : null ];
216 }
217 }
218 return [ 'success' => true, 'silent_forms' => $silent, 'message' => sprintf( '%d silent form(s) over %d days.', count( $silent ), $days ) ];
219 },
220 'permission_callback' => function() use ( $self ) { return $self->can_view_forms(); },
221 'meta' => $this->ro_meta(),
222 ] );
223 }
224
225 /* -------------------------- export-entries ------------------------- */
226
227 private function register_export_entries() {
228 $self = $this;
229 wp_register_ability( 'atarim/gravity-export-entries', [
230 'label' => 'Export Gravity Entries', 'category' => 'atarim',
231 'description' => 'Export a form\'s entries as CSV text (header row from field labels). Capped via limit (default 1000). Excludes spam/trash unless include_spam:true.',
232 '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 ],
233 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'csv' => [ 'type' => 'string' ], 'rows' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
234 'execute_callback' => function( $input = [] ) {
235 $form_id = (int) $input['form_id'];
236 $form = GFAPI::get_form( $form_id );
237 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
238 $search = empty( $input['include_spam'] ) ? [ 'status' => 'active' ] : [];
239 $limit = isset( $input['limit'] ) ? max( 1, (int) $input['limit'] ) : 1000;
240 $raw = GFAPI::get_entries( $form_id, $search, [ 'key' => 'date_created', 'direction' => 'DESC' ], [ 'offset' => 0, 'page_size' => $limit ] );
241 if ( is_wp_error( $raw ) ) { return [ 'success' => false, 'message' => $raw->get_error_message() ]; }
242 $labels = [ 'Entry ID', 'Submitted' ];
243 $field_ids = [];
244 if ( isset( $form['fields'] ) && is_array( $form['fields'] ) ) {
245 foreach ( $form['fields'] as $f ) {
246 $fid = is_object( $f ) ? (string) $f->id : ( isset( $f['id'] ) ? (string) $f['id'] : '' );
247 $lbl = is_object( $f ) ? (string) $f->label : ( isset( $f['label'] ) ? (string) $f['label'] : '' );
248 if ( $fid === '' ) { continue; }
249 $field_ids[] = $fid; $labels[] = $lbl !== '' ? $lbl : ( 'Field ' . $fid );
250 }
251 }
252 $lines = [ self::csv_row( $labels ) ];
253 foreach ( (array) $raw as $e ) {
254 $row = [ isset( $e['id'] ) ? (string) $e['id'] : '', isset( $e['date_created'] ) ? (string) $e['date_created'] : '' ];
255 foreach ( $field_ids as $fid ) { $row[] = isset( $e[ $fid ] ) ? (string) $e[ $fid ] : ''; }
256 $lines[] = self::csv_row( $row );
257 }
258 return [ 'success' => true, 'csv' => implode( "\n", $lines ), 'rows' => count( $lines ) - 1, 'message' => sprintf( '%d row(s) exported.', count( $lines ) - 1 ) ];
259 },
260 'permission_callback' => function() use ( $self ) { return $self->can_view_entries(); },
261 'meta' => $this->ro_meta(),
262 ] );
263 }
264
265 private static function csv_row( $cells ) {
266 $out = [];
267 foreach ( (array) $cells as $c ) { $out[] = '"' . str_replace( '"', '""', (string) $c ) . '"'; }
268 return implode( ',', $out );
269 }
270
271 /* -------------------------- mark-entry-spam ------------------------ */
272
273 private function register_mark_entry_spam() {
274 $self = $this;
275 wp_register_ability( 'atarim/gravity-mark-entry-spam', [
276 'label' => 'Mark Gravity Entry Spam', 'category' => 'atarim',
277 'description' => 'Flag an entry as spam (or clear it with is_spam:false).',
278 'input_schema' => [ 'type' => 'object', 'properties' => [ 'entry_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'is_spam' => [ 'type' => 'boolean', 'default' => true ] ], 'required' => [ 'entry_id' ], 'additionalProperties' => false ],
279 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
280 'execute_callback' => function( $input = [] ) {
281 $entry = GFAPI::get_entry( (int) $input['entry_id'] );
282 if ( is_wp_error( $entry ) || ! $entry ) { return [ 'success' => false, 'message' => 'Entry not found.' ]; }
283 $is_spam = ! isset( $input['is_spam'] ) || ! empty( $input['is_spam'] );
284 $entry['status'] = $is_spam ? 'spam' : 'active';
285 $r = GFAPI::update_entry( $entry );
286 if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => 'Update failed: ' . $r->get_error_message() ]; }
287 return [ 'success' => true, 'message' => $is_spam ? 'Entry marked as spam.' : 'Entry unmarked from spam.' ];
288 },
289 'permission_callback' => function() use ( $self ) { return $self->can_edit_entries(); },
290 'meta' => $this->write_meta( false ),
291 ] );
292 }
293
294 /* ------------------------ update-notifications ---------------------- */
295
296 private function register_update_notifications() {
297 $self = $this;
298 wp_register_ability( 'atarim/gravity-update-notifications', [
299 'label' => 'Update Gravity Notification Recipients', 'category' => 'atarim',
300 'description' => 'Update the recipient email(s) of one or more of a form\'s notifications. updates: [{ notification_id, recipients: [emails] }]. Emails are sanitized; invalid/empty sets are skipped and reported.',
301 '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 ],
302 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'updated' => [ 'type' => 'array' ], 'skipped' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
303 'execute_callback' => function( $input = [] ) {
304 $form = GFAPI::get_form( (int) $input['form_id'] );
305 if ( ! $form ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
306 $updated = []; $skipped = [];
307 foreach ( (array) $input['updates'] as $u ) {
308 $nid = isset( $u['notification_id'] ) ? (string) $u['notification_id'] : '';
309 if ( ! isset( $form['notifications'][ $nid ] ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'Not found on this form.' ]; continue; }
310 $clean = array_filter( array_map( 'sanitize_email', isset( $u['recipients'] ) ? (array) $u['recipients'] : [] ) );
311 if ( empty( $clean ) ) { $skipped[] = [ 'notification_id' => $nid, 'reason' => 'No valid recipient emails.' ]; continue; }
312 $form['notifications'][ $nid ]['to'] = implode( ',', $clean );
313 $form['notifications'][ $nid ]['toType'] = 'email';
314 $updated[] = $nid;
315 }
316 if ( ! empty( $updated ) ) {
317 $r = GFAPI::update_form( $form );
318 if ( is_wp_error( $r ) ) { return [ 'success' => false, 'updated' => [], 'skipped' => $skipped, 'message' => 'Update failed: ' . $r->get_error_message() ]; }
319 }
320 return [ 'success' => ! empty( $updated ), 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( '%d updated, %d skipped.', count( $updated ), count( $skipped ) ) ];
321 },
322 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
323 'meta' => $this->write_meta( false ),
324 ] );
325 }
326
327 /* -------------------------- duplicate-form ------------------------- */
328
329 private function register_duplicate_form() {
330 $self = $this;
331 wp_register_ability( 'atarim/gravity-duplicate-form', [
332 'label' => 'Duplicate Gravity Form', 'category' => 'atarim',
333 'description' => 'Clone a form (fields, notifications, confirmations, settings) into a new form. Optional new_title (defaults to "<title> (Copy)").',
334 'input_schema' => [ 'type' => 'object', 'properties' => [ 'form_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'new_title' => [ 'type' => 'string' ] ], 'required' => [ 'form_id' ], 'additionalProperties' => false ],
335 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'new_form_id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
336 'execute_callback' => function( $input = [] ) {
337 $original = GFAPI::get_form( (int) $input['form_id'] );
338 if ( ! $original ) { return [ 'success' => false, 'message' => 'Form not found.' ]; }
339 $new = $original; unset( $new['id'] );
340 $new['title'] = ( isset( $input['new_title'] ) && $input['new_title'] !== '' ) ? sanitize_text_field( $input['new_title'] ) : ( isset( $original['title'] ) ? $original['title'] . ' (Copy)' : 'Form Copy' );
341 $new_id = GFAPI::add_form( $new );
342 if ( is_wp_error( $new_id ) ) { return [ 'success' => false, 'message' => 'Duplicate failed: ' . $new_id->get_error_message() ]; }
343 return [ 'success' => true, 'new_form_id' => (int) $new_id, 'message' => sprintf( 'Duplicated as form %d.', (int) $new_id ) ];
344 },
345 'permission_callback' => function() use ( $self ) { return $self->can_edit_forms(); },
346 'meta' => $this->write_meta( false ),
347 ] );
348 }
349 }
350