PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.5.2
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.5.2
3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / includes / abilities / class-evf-abilities-handlers.php
everest-forms / includes / abilities Last commit date
class-evf-abilities-handlers.php 3 weeks ago class-evf-abilities-registry.php 3 weeks ago class-evf-abilities.php 3 weeks ago class-evf-field-schemas.php 3 weeks ago class-evf-form-builder.php 3 weeks ago class-evf-mcp-server.php 3 weeks ago
class-evf-abilities-handlers.php
1035 lines
1 <?php
2 /**
3 * Ability handler callbacks.
4 *
5 * @package EverestForms\Abilities
6 */
7
8 defined( 'ABSPATH' ) || exit;
9
10 /**
11 * Static handlers for each registered ability.
12 *
13 * Every callback receives the validated input array and returns a plain
14 * array/scalar on success or a WP_Error on failure.
15 */
16 class EVF_Abilities_Handlers {
17
18 /* ------------------------------------------------------------------
19 * Permission helpers
20 * ------------------------------------------------------------------ */
21
22 public static function can_view_forms() {
23 return current_user_can( 'everest_forms_view_forms' ) || current_user_can( 'manage_everest_forms' );
24 }
25
26 public static function can_create_forms() {
27 return current_user_can( 'everest_forms_create_forms' );
28 }
29
30 public static function can_view_entries() {
31 return current_user_can( 'everest_forms_view_entries' ) || current_user_can( 'manage_everest_forms' );
32 }
33
34 public static function can_delete_entries() {
35 return current_user_can( 'everest_forms_delete_entries' ) || current_user_can( 'manage_everest_forms' );
36 }
37
38 public static function can_edit_entries() {
39 return current_user_can( 'everest_forms_edit_entries' ) || current_user_can( 'manage_everest_forms' );
40 }
41
42 public static function can_edit_forms() {
43 return current_user_can( 'everest_forms_edit_forms' ) || current_user_can( 'manage_everest_forms' );
44 }
45
46 public static function can_delete_forms() {
47 return current_user_can( 'everest_forms_delete_forms' ) || current_user_can( 'manage_everest_forms' );
48 }
49
50 public static function can_activate_plugins() {
51 return current_user_can( 'activate_plugins' );
52 }
53
54 /* ------------------------------------------------------------------
55 * Form abilities
56 * ------------------------------------------------------------------ */
57
58 /**
59 * @param array $input Args.
60 * @return array
61 */
62 public static function list_forms( $input ) {
63 $limit = isset( $input['limit'] ) ? (int) $input['limit'] : 50;
64 $status = isset( $input['status'] ) ? (string) $input['status'] : 'publish';
65
66 $query_args = array(
67 'post_type' => 'everest_form',
68 'posts_per_page' => $limit,
69 'orderby' => 'date',
70 'order' => 'DESC',
71 );
72 if ( 'any' !== $status ) {
73 $query_args['post_status'] = $status;
74 } else {
75 $query_args['post_status'] = array( 'publish', 'draft', 'trash' );
76 }
77
78 $forms = get_posts( $query_args );
79 $out = array();
80 foreach ( $forms as $form ) {
81 $out[] = array(
82 'id' => (int) $form->ID,
83 'title' => $form->post_title,
84 'status' => $form->post_status,
85 'created' => $form->post_date_gmt,
86 );
87 }
88 return $out;
89 }
90
91 /**
92 * @param array $input Args.
93 * @return array|WP_Error
94 */
95 public static function get_form( $input ) {
96 $form_id = (int) $input['form_id'];
97 $form = evf()->form->get( $form_id );
98 if ( ! $form ) {
99 return new WP_Error( 'evf_form_not_found', 'Form not found.', array( 'status' => 404 ) );
100 }
101 $data = is_string( $form->post_content ) ? json_decode( $form->post_content, true ) : array();
102 if ( ! is_array( $data ) ) {
103 $data = array();
104 }
105 // Surface the parts of the post_content the LLM needs to make sensible
106 // follow-up edits: settings (incl. multi-part/conversational toggles),
107 // fields config, the structure map that says where each field lives,
108 // and the multi_part array when present. Without structure+multi_part
109 // the LLM can't reliably know how a form is laid out.
110 return array(
111 'id' => (int) $form->ID,
112 'title' => $form->post_title,
113 'status' => $form->post_status,
114 'settings' => isset( $data['settings'] ) ? $data['settings'] : array(),
115 'fields' => isset( $data['form_fields'] ) ? $data['form_fields'] : array(),
116 'structure' => isset( $data['structure'] ) ? $data['structure'] : array(),
117 'multi_part' => isset( $data['multi_part'] ) ? $data['multi_part'] : array(),
118 );
119 }
120
121 /**
122 * @param array $input Args.
123 * @return array|WP_Error
124 */
125 public static function create_form( $input ) {
126 $title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : '';
127 $template = isset( $input['template'] ) ? sanitize_key( $input['template'] ) : 'blank';
128 $dry_run = ! empty( $input['dry_run'] );
129 // Default to inactive so AI-built forms don't go live until the user
130 // explicitly publishes them. EVF marks a form inactive with BOTH a
131 // custom `inactive` post_status AND `form_enabled = 0` in the form
132 // data (the toggle in the All Forms list reads form_enabled). Accept
133 // "draft" as an alias for "inactive".
134 $status = isset( $input['status'] ) ? sanitize_key( (string) $input['status'] ) : 'inactive';
135 if ( 'draft' === $status ) {
136 $status = 'inactive';
137 }
138 if ( ! in_array( $status, array( 'inactive', 'publish' ), true ) ) {
139 $status = 'inactive';
140 }
141 $is_active = ( 'publish' === $status );
142
143 if ( '' === $title ) {
144 return new WP_Error( 'evf_invalid_title', 'A non-empty title is required.', array( 'status' => 400 ) );
145 }
146
147 $descriptor = array_intersect_key( $input, array_flip( array( 'title', 'description', 'settings', 'fields', 'layout', 'multi_part', 'conversational' ) ) );
148
149 // Validate the descriptor against the schema registry BEFORE touching
150 // the database, so a bad field type doesn't leave an empty form on
151 // disk. dry_run can use the same throwaway build to return a preview.
152 $preview = EVF_Form_Builder::build( array(), $descriptor );
153 if ( is_wp_error( $preview ) ) {
154 return $preview;
155 }
156
157 if ( $dry_run ) {
158 return array(
159 'dry_run' => true,
160 'preview' => array(
161 'title' => isset( $preview['settings']['form_title'] ) ? $preview['settings']['form_title'] : $title,
162 'fields' => self::summarize_fields( $preview ),
163 'field_count' => isset( $preview['form_fields'] ) ? count( $preview['form_fields'] ) : 0,
164 'structure' => isset( $preview['structure'] ) ? $preview['structure'] : array(),
165 ),
166 );
167 }
168
169 $form_id = evf()->form->create( $title, $template );
170 if ( ! $form_id ) {
171 return new WP_Error( 'evf_create_failed', 'Form creation failed (insufficient capabilities or invalid template).', array( 'status' => 500 ) );
172 }
173
174 // Merge what the template produced (if any) with what we built. This
175 // is the canonical build — produces the final shape, validates again
176 // in case the template added something incompatible (shouldn't, but
177 // defensive). If this fails, clean up the empty form we just made.
178 $created = evf()->form->get( $form_id );
179 $base = is_string( $created->post_content ) ? json_decode( $created->post_content, true ) : array();
180 if ( ! is_array( $base ) ) {
181 $base = array();
182 }
183 $merged = EVF_Form_Builder::build( $base, $descriptor );
184 if ( is_wp_error( $merged ) ) {
185 wp_delete_post( $form_id, true );
186 return $merged;
187 }
188 // EVF's "active/inactive" state lives in the top-level `form_enabled`
189 // flag (the toggle in the All Forms list reads it, defaulting to 1).
190 // Set it in the form data so the merged save persists the right state.
191 $merged['form_enabled'] = $is_active ? 1 : 0;
192 $merged['id'] = (int) $form_id;
193 evf()->form->update( $form_id, $merged );
194
195 // EVF's form->create() always publishes. Mirror the active/inactive
196 // state into post_status too (EVF uses a custom `inactive` status,
197 // not `draft`), so both the toggle and the status filter agree.
198 wp_update_post( array( 'ID' => (int) $form_id, 'post_status' => $is_active ? 'publish' : 'inactive' ) );
199
200 return array(
201 'id' => (int) $form_id,
202 'title' => isset( $merged['settings']['form_title'] ) ? $merged['settings']['form_title'] : $title,
203 'status' => $status,
204 'active' => $is_active,
205 'fields' => self::summarize_fields( $merged ),
206 'edit_url' => admin_url( 'admin.php?page=evf-builder&tab=fields&form_id=' . (int) $form_id ),
207 );
208 }
209
210 /**
211 * Summarize fields for a response envelope (no internal IDs in the verbose form).
212 *
213 * @param array $data Built form-data array.
214 * @return array
215 */
216 protected static function summarize_fields( $data ) {
217 $out = array();
218 if ( empty( $data['form_fields'] ) ) {
219 return $out;
220 }
221 foreach ( $data['form_fields'] as $fid => $f ) {
222 $out[] = array(
223 'id' => (string) $fid,
224 'type' => isset( $f['type'] ) ? $f['type'] : '',
225 'label' => isset( $f['label'] ) ? $f['label'] : '',
226 );
227 }
228 return $out;
229 }
230
231 /**
232 * @param array $input Args.
233 * @return array|WP_Error
234 */
235 public static function update_form( $input ) {
236 $form_id = (int) $input['form_id'];
237 $dry_run = ! empty( $input['dry_run'] );
238 $form = evf()->form->get( $form_id );
239 if ( ! $form ) {
240 return new WP_Error( 'evf_form_not_found', 'Form not found.', array( 'status' => 404 ) );
241 }
242
243 $existing = is_string( $form->post_content ) ? json_decode( $form->post_content, true ) : array();
244 if ( ! is_array( $existing ) ) {
245 $existing = array();
246 }
247
248 $descriptor = array_intersect_key( $input, array_flip( array( 'title', 'description', 'settings', 'fields', 'layout', 'form_fields_patch', 'multi_part', 'conversational' ) ) );
249
250 $next = EVF_Form_Builder::build( $existing, $descriptor );
251 if ( is_wp_error( $next ) ) {
252 return $next;
253 }
254 $next['id'] = $form_id;
255
256 if ( $dry_run ) {
257 return array(
258 'dry_run' => true,
259 'form_id' => $form_id,
260 'preview' => array(
261 'title' => isset( $next['settings']['form_title'] ) ? $next['settings']['form_title'] : '',
262 'fields' => self::summarize_fields( $next ),
263 'field_count' => isset( $next['form_fields'] ) ? count( $next['form_fields'] ) : 0,
264 'structure' => isset( $next['structure'] ) ? $next['structure'] : array(),
265 ),
266 );
267 }
268
269 $result = evf()->form->update( $form_id, $next );
270 if ( ! $result ) {
271 return new WP_Error( 'evf_form_update_failed', 'Form update failed (likely a capability check).', array( 'status' => 403 ) );
272 }
273
274 return array(
275 'form_id' => (int) $result,
276 'title' => isset( $next['settings']['form_title'] ) ? $next['settings']['form_title'] : '',
277 'fields' => self::summarize_fields( $next ),
278 'edit_url' => admin_url( 'admin.php?page=evf-builder&tab=fields&form_id=' . (int) $result ),
279 );
280 }
281
282 /**
283 * @param array $input Args.
284 * @return array
285 */
286 /**
287 * Activate an installed Everest Forms addon.
288 *
289 * @param array $input Args.
290 * @return array|WP_Error
291 */
292 public static function activate_addon( $input ) {
293 $plugin = isset( $input['plugin'] ) ? (string) $input['plugin'] : '';
294 if ( '' === $plugin || false === strpos( $plugin, '/' ) ) {
295 return new WP_Error( 'evf_invalid_plugin', 'Plugin file is required, e.g. "everest-forms-survey-polls-quiz/everest-forms-survey-polls-quiz.php".', array( 'status' => 400 ) );
296 }
297
298 // Refuse to activate anything outside the EVF family — this ability is
299 // scoped to Everest Forms addons, not a general plugin activator.
300 if ( 0 !== strpos( $plugin, 'everest-forms' ) ) {
301 return new WP_Error( 'evf_not_an_evf_addon', 'This ability only activates Everest Forms addons.', array( 'status' => 400 ) );
302 }
303
304 if ( ! function_exists( 'is_plugin_active' ) ) {
305 include_once ABSPATH . 'wp-admin/includes/plugin.php';
306 }
307
308 $installed = (array) get_plugins();
309 if ( ! isset( $installed[ $plugin ] ) ) {
310 return new WP_Error(
311 'evf_addon_not_installed',
312 sprintf( 'Addon "%s" is not installed on this site. The user needs to install/purchase it from their Everest Forms account, or upgrade their plan if it is not included.', $plugin ),
313 array(
314 'status' => 404,
315 'plugin' => $plugin,
316 'action' => 'install_or_upgrade',
317 'account_url' => 'https://everestforms.net/my-account/downloads/',
318 )
319 );
320 }
321
322 if ( is_plugin_active( $plugin ) ) {
323 return array(
324 'plugin' => $plugin,
325 'active' => true,
326 'message' => 'Addon was already active.',
327 );
328 }
329
330 // Explicit-confirmation gate. Activating a plugin changes site state,
331 // so we never do it on the first call. The caller must come back with
332 // confirm:true after the user has actually agreed. This guarantees a
333 // confirmation step server-side, independent of whether the MCP client
334 // shows its own approval dialog (the user may have "always allow" set).
335 $confirmed = ! empty( $input['confirm'] ) && true === filter_var( $input['confirm'], FILTER_VALIDATE_BOOLEAN );
336 if ( ! $confirmed ) {
337 $name = isset( $installed[ $plugin ]['Name'] ) ? $installed[ $plugin ]['Name'] : $plugin;
338 return array(
339 'status' => 'confirmation_required',
340 'plugin' => $plugin,
341 'addon' => $name,
342 'requires_confirm' => true,
343 'message' => sprintf( 'Activating the "%s" addon will change this site. Ask the user to confirm, then call activate-addon again with confirm:true to proceed. Do NOT activate without the user\'s explicit agreement.', $name ),
344 );
345 }
346
347 $result = activate_plugin( $plugin, '', false, false );
348 if ( is_wp_error( $result ) ) {
349 return new WP_Error(
350 'evf_addon_activation_failed',
351 $result->get_error_message(),
352 array( 'status' => 500, 'plugin' => $plugin )
353 );
354 }
355
356 // EVF has a second gate beyond WP plugin activation: the slug must be
357 // present in the `everest_forms_enabled_features` option for some addon
358 // field types (payment-coupon, payment-square, etc.) to be registered.
359 // Derive the slug from the plugin file (folder name) and add it.
360 $slug = dirname( $plugin );
361 $enabled_features = (array) get_option( 'everest_forms_enabled_features', array() );
362 $feature_added = false;
363 if ( $slug && ! in_array( $slug, $enabled_features, true ) ) {
364 $enabled_features[] = $slug;
365 update_option( 'everest_forms_enabled_features', $enabled_features );
366 $feature_added = true;
367 }
368
369 return array(
370 'plugin' => $plugin,
371 'active' => true,
372 'feature_slug' => $slug,
373 'feature_added' => $feature_added,
374 'name' => isset( $installed[ $plugin ]['Name'] ) ? $installed[ $plugin ]['Name'] : $plugin,
375 'version' => isset( $installed[ $plugin ]['Version'] ) ? $installed[ $plugin ]['Version'] : null,
376 'message' => 'Addon activated and registered in Everest Forms enabled-features. Retry the original create-form/update-form call now.',
377 );
378 }
379
380 public static function list_addons( $input ) {
381 $known = array(
382 'everest-forms-pro/everest-forms-pro.php' => 'Everest Forms PRO',
383 'everest-forms-survey-polls-quiz/everest-forms-survey-polls-quiz.php' => 'Survey, Polls and Quiz',
384 'everest-forms-multi-part/everest-forms-multi-part.php' => 'Multi-Part Forms',
385 'everest-forms-repeater-fields/everest-forms-repeater-fields.php' => 'Repeater Fields',
386 'everest-forms-conversational-forms/everest-forms-conversational-forms.php' => 'Conversational Forms',
387 'everest-forms-coupons/everest-forms-coupons.php' => 'Coupons',
388 'everest-forms-captcha/everest-forms-captcha.php' => 'Captcha',
389 'everest-forms-calculations/everest-forms-calculations.php' => 'Calculations',
390 'everest-forms-save-and-continue/everest-forms-save-and-continue.php' => 'Save and Continue',
391 'everest-forms-pdf-submission/everest-forms-pdf-submission.php' => 'PDF Submission',
392 'everest-forms-google-sheets/everest-forms-google-sheets.php' => 'Google Sheets',
393 'everest-forms-mailchimp/everest-forms-mailchimp.php' => 'Mailchimp',
394 'everest-forms-zapier/everest-forms-zapier.php' => 'Zapier',
395 'everest-forms-stripe/everest-forms-stripe.php' => 'Stripe',
396 'everest-forms-paypal-standard/everest-forms-paypal-standard.php' => 'PayPal Standard',
397 'everest-forms-form-analytics/everest-forms-form-analytics.php' => 'Form Analytics',
398 'everest-forms-user-registration/everest-forms-user-registration.php' => 'User Registration',
399 'everest-forms-post-submissions/everest-forms-post-submissions.php' => 'Post Submissions',
400 'everest-forms-email-templates/everest-forms-email-templates.php' => 'Email Templates',
401 'everest-forms-frontend-listing/everest-forms-frontend-listing.php' => 'Frontend Listing',
402 'everest-forms-style-customizer/everest-forms-style-customizer.php' => 'Style Customizer',
403 'everest-forms-sms-notifications/everest-forms-sms-notifications.php' => 'SMS Notifications',
404 );
405
406 if ( ! function_exists( 'is_plugin_active' ) ) {
407 include_once ABSPATH . 'wp-admin/includes/plugin.php';
408 }
409
410 // Addons whose field types are gated by EVF's `everest_forms_enabled_features`
411 // option in addition to WP plugin activation. For these, the slug MUST be
412 // in enabled_features for the addon's fields to register. For everything
413 // else (PRO core, Multi-Part, Captcha, Conversational, etc.) the WP plugin
414 // active state alone determines operationality.
415 $feature_gated_slugs = array( 'everest-forms-coupons', 'everest-forms-square' );
416
417 $installed = function_exists( 'get_plugins' ) ? (array) get_plugins() : array();
418 $enabled_features = (array) get_option( 'everest_forms_enabled_features', array() );
419 $out = array();
420 foreach ( $known as $file => $name ) {
421 $is_installed = isset( $installed[ $file ] );
422 $slug = dirname( $file );
423 $active = function_exists( 'is_plugin_active' ) ? is_plugin_active( $file ) : false;
424 $feature_on = in_array( $slug, $enabled_features, true );
425 $gated = in_array( $slug, $feature_gated_slugs, true );
426
427 // Compute correctly: feature-gated addons need installed+active+feature_on;
428 // non-gated addons just need installed+active.
429 $fully = $is_installed && $active && ( ! $gated || $feature_on );
430
431 $out[] = array(
432 'plugin' => $file,
433 'name' => $name,
434 'installed' => $is_installed,
435 'active' => $active,
436 'feature_gated' => $gated,
437 'evf_feature_on' => $feature_on,
438 'fully_operational' => $fully,
439 'version' => $is_installed && isset( $installed[ $file ]['Version'] ) ? $installed[ $file ]['Version'] : null,
440 );
441 }
442 return $out;
443 }
444
445 /**
446 * @param array $input Args.
447 * @return array|WP_Error
448 */
449 public static function describe_field_type( $input ) {
450 $type = isset( $input['type'] ) ? sanitize_key( (string) $input['type'] ) : '';
451 if ( '' === $type ) {
452 return new WP_Error( 'evf_invalid_type', 'Missing "type".', array( 'status' => 400 ) );
453 }
454 $registered = EVF_Field_Schemas::registered_types();
455 $schema = EVF_Field_Schemas::for_type( $type );
456 $addon = EVF_Field_Schemas::addon_for( $type );
457
458 // `available` reflects whether the field class is registered in core
459 // (true even for likert/yes-no/rating whose stubs live in core but
460 // whose real behavior ships in an addon). `usable_now` is the more
461 // useful "will the form builder accept this right now?" flag,
462 // matching exactly what validate_field() in the builder enforces:
463 // class registered AND (no addon dependency OR addon plugin active).
464 $class_registered = isset( $registered[ $type ] );
465 $addon_active = true;
466 if ( $addon && ! empty( $addon['plugin'] ) ) {
467 if ( ! function_exists( 'is_plugin_active' ) ) {
468 include_once ABSPATH . 'wp-admin/includes/plugin.php';
469 }
470 $addon_active = function_exists( 'is_plugin_active' ) ? is_plugin_active( $addon['plugin'] ) : false;
471 }
472
473 return array(
474 'type' => $type,
475 'available' => $class_registered,
476 'usable_now' => $class_registered && $addon_active,
477 'addon' => $addon,
478 'addon_active' => $addon_active,
479 'accepted_keys' => $schema ? $schema['accepted_keys'] : array(),
480 'requires_choices' => $schema ? $schema['requires_choices'] : false,
481 'known_to_builder' => null !== $schema,
482 );
483 }
484
485 /**
486 * @param array $input Args.
487 * @return array|WP_Error
488 */
489 public static function update_form_status( $input ) {
490 $form_id = (int) $input['form_id'];
491 $status = isset( $input['status'] ) ? sanitize_key( (string) $input['status'] ) : '';
492 if ( ! in_array( $status, array( 'publish', 'draft', 'trash' ), true ) ) {
493 return new WP_Error( 'evf_invalid_status', 'Status must be one of: publish, draft, trash.', array( 'status' => 400 ) );
494 }
495 $post = get_post( $form_id );
496 if ( ! $post || 'everest_form' !== $post->post_type ) {
497 return new WP_Error( 'evf_form_not_found', 'Form not found.', array( 'status' => 404 ) );
498 }
499 if ( $post->post_status === $status ) {
500 return array( 'form_id' => $form_id, 'status' => $status, 'changed' => false );
501 }
502 $result = wp_update_post( array( 'ID' => $form_id, 'post_status' => $status ), true );
503 if ( is_wp_error( $result ) ) {
504 return new WP_Error( 'evf_form_status_update_failed', $result->get_error_message(), array( 'status' => 500 ) );
505 }
506 return array( 'form_id' => $form_id, 'status' => $status, 'changed' => true );
507 }
508
509 /**
510 * @param array $input Args.
511 * @return array|WP_Error
512 */
513 public static function bulk_delete_entries( $input ) {
514 global $wpdb;
515 $ids = isset( $input['entry_ids'] ) && is_array( $input['entry_ids'] ) ? array_values( array_unique( array_filter( array_map( 'intval', $input['entry_ids'] ) ) ) ) : array();
516 $permanent = array_key_exists( 'permanent', $input ) ? (bool) $input['permanent'] : true;
517
518 if ( empty( $ids ) ) {
519 return new WP_Error( 'evf_no_ids', 'Provide one or more entry_ids.', array( 'status' => 400 ) );
520 }
521
522 $deleted = 0;
523 $skipped = array();
524
525 if ( ! $permanent ) {
526 // Single UPDATE for the soft-delete case — much cheaper than N round-trips.
527 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
528 $query = "UPDATE {$wpdb->prefix}evf_entries SET status = 'trash' WHERE entry_id IN ({$placeholders})";
529 $result = $wpdb->query( $wpdb->prepare( $query, $ids ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
530 if ( false === $result ) {
531 return new WP_Error( 'evf_bulk_trash_failed', 'Bulk trash failed.', array( 'status' => 500 ) );
532 }
533 return array(
534 'trashed' => (int) $result,
535 'requested' => count( $ids ),
536 'permanent' => false,
537 );
538 }
539
540 foreach ( $ids as $entry_id ) {
541 $d = $wpdb->delete( $wpdb->prefix . 'evf_entries', array( 'entry_id' => $entry_id ), array( '%d' ) );
542 if ( false === $d || 0 === (int) $d ) {
543 $skipped[] = $entry_id;
544 continue;
545 }
546 $wpdb->delete( $wpdb->prefix . 'evf_entrymeta', array( 'entry_id' => $entry_id ), array( '%d' ) );
547 do_action( 'everest_forms_delete_entry', $entry_id );
548 $deleted++;
549 }
550
551 return array(
552 'deleted' => $deleted,
553 'requested' => count( $ids ),
554 'skipped' => $skipped,
555 'permanent' => true,
556 );
557 }
558
559 /**
560 * @param array $input Args.
561 * @return array|WP_Error
562 */
563 public static function delete_form( $input ) {
564 $form_id = (int) $input['form_id'];
565 $post = get_post( $form_id );
566 if ( ! $post || 'everest_form' !== $post->post_type ) {
567 return new WP_Error( 'evf_form_not_found', 'Form not found.', array( 'status' => 404 ) );
568 }
569 // We've already gated on `can_delete_forms` in the permission_callback,
570 // so bypass EVF_Form_Handler::delete()'s 'everest_forms_delete' cap check
571 // (singular cap that isn't granted by default) and delete directly.
572 $deleted = wp_delete_post( $form_id, true );
573 if ( ! $deleted ) {
574 return new WP_Error( 'evf_form_delete_failed', 'wp_delete_post failed.', array( 'status' => 500 ) );
575 }
576 do_action( 'everest_forms_delete_form', array( $form_id ) );
577 return array( 'deleted' => true, 'form_id' => $form_id );
578 }
579
580 /**
581 * @param array $input Args.
582 * @return array|WP_Error
583 */
584 public static function duplicate_form( $input ) {
585 $form_id = (int) $input['form_id'];
586 $result = evf()->form->duplicate( array( $form_id ) );
587 if ( ! $result ) {
588 return new WP_Error( 'evf_form_duplicate_failed', 'Form could not be duplicated.', array( 'status' => 500 ) );
589 }
590 $new_id = is_array( $result ) ? (int) reset( $result ) : (int) $result;
591 return array(
592 'source_form_id' => $form_id,
593 'new_form_id' => $new_id,
594 );
595 }
596
597 /**
598 * @param array $input Args.
599 * @return array
600 */
601 public static function list_field_types( $input ) {
602 $out = array();
603 $registry = evf()->form_fields;
604 $groups = ( is_object( $registry ) && isset( $registry->form_fields ) ) ? (array) $registry->form_fields : array();
605
606 foreach ( $groups as $group => $fields ) {
607 foreach ( (array) $fields as $field ) {
608 if ( ! is_object( $field ) ) {
609 continue;
610 }
611 $out[] = array(
612 'type' => isset( $field->type ) ? $field->type : '',
613 'name' => isset( $field->name ) ? $field->name : '',
614 'group' => (string) $group,
615 'icon' => isset( $field->icon ) ? $field->icon : '',
616 );
617 }
618 }
619 return $out;
620 }
621
622 /* ------------------------------------------------------------------
623 * Entry abilities
624 * ------------------------------------------------------------------ */
625
626 /**
627 * @param array $input Args.
628 * @return array
629 */
630 public static function list_entries( $input ) {
631 $args = array(
632 'form_id' => (int) $input['form_id'],
633 'limit' => isset( $input['limit'] ) ? (int) $input['limit'] : 25,
634 'offset' => isset( $input['offset'] ) ? (int) $input['offset'] : 0,
635 );
636 if ( ! empty( $input['search'] ) ) {
637 $args['search'] = (string) $input['search'];
638 }
639 if ( ! empty( $input['status'] ) ) {
640 $args['status'] = (string) $input['status'];
641 }
642 $ids = evf_search_entries( $args );
643 $out = array();
644 foreach ( (array) $ids as $entry_id ) {
645 $entry = function_exists( 'evf_get_entry' ) ? evf_get_entry( (int) $entry_id ) : null;
646 if ( ! $entry ) {
647 continue;
648 }
649 $out[] = array(
650 'entry_id' => (int) $entry->entry_id,
651 'form_id' => (int) $entry->form_id,
652 'status' => isset( $entry->status ) ? $entry->status : '',
653 'date' => isset( $entry->date_created ) ? $entry->date_created : '',
654 );
655 }
656 return $out;
657 }
658
659 /**
660 * @param array $input Args.
661 * @return array|WP_Error
662 */
663 public static function get_entry( $input ) {
664 $entry_id = (int) $input['entry_id'];
665 $entry = function_exists( 'evf_get_entry' ) ? evf_get_entry( $entry_id, true ) : null;
666 if ( ! $entry ) {
667 return new WP_Error( 'evf_entry_not_found', 'Entry not found.', array( 'status' => 404 ) );
668 }
669 if ( ! self::can_view_entries() ) {
670 return new WP_Error( 'evf_forbidden', 'Permission denied.', array( 'status' => 403 ) );
671 }
672 $meta = isset( $entry->meta ) ? $entry->meta : array();
673 return array(
674 'entry_id' => (int) $entry->entry_id,
675 'form_id' => (int) $entry->form_id,
676 'status' => isset( $entry->status ) ? $entry->status : '',
677 'date' => isset( $entry->date_created ) ? $entry->date_created : '',
678 'fields' => $meta,
679 );
680 }
681
682 /**
683 * @param array $input Args.
684 * @return array|WP_Error
685 */
686 public static function delete_entry( $input ) {
687 global $wpdb;
688 $entry_id = (int) $input['entry_id'];
689 $permanent = array_key_exists( 'permanent', $input ) ? (bool) $input['permanent'] : true;
690
691 if ( ! $permanent ) {
692 $updated = $wpdb->update(
693 $wpdb->prefix . 'evf_entries',
694 array( 'status' => 'trash' ),
695 array( 'entry_id' => $entry_id ),
696 array( '%s' ),
697 array( '%d' )
698 );
699 if ( false === $updated ) {
700 return new WP_Error( 'evf_entry_trash_failed', 'Entry could not be moved to trash.', array( 'status' => 500 ) );
701 }
702 return array( 'trashed' => true, 'entry_id' => $entry_id );
703 }
704
705 $deleted = $wpdb->delete( $wpdb->prefix . 'evf_entries', array( 'entry_id' => $entry_id ), array( '%d' ) );
706 if ( false === $deleted || 0 === (int) $deleted ) {
707 return new WP_Error( 'evf_entry_delete_failed', 'Entry could not be deleted (not found or DB error).', array( 'status' => 404 ) );
708 }
709 $wpdb->delete( $wpdb->prefix . 'evf_entrymeta', array( 'entry_id' => $entry_id ), array( '%d' ) );
710 do_action( 'everest_forms_delete_entry', $entry_id );
711 return array( 'deleted' => true, 'entry_id' => $entry_id );
712 }
713
714 /**
715 * @param array $input Args.
716 * @return array|WP_Error
717 */
718 public static function update_entry_status( $input ) {
719 global $wpdb;
720 $entry_id = (int) $input['entry_id'];
721 $status = sanitize_key( $input['status'] );
722 $updated = $wpdb->update(
723 $wpdb->prefix . 'evf_entries',
724 array( 'status' => $status ),
725 array( 'entry_id' => $entry_id ),
726 array( '%s' ),
727 array( '%d' )
728 );
729 if ( false === $updated ) {
730 return new WP_Error( 'evf_entry_status_failed', 'Could not update entry status.', array( 'status' => 500 ) );
731 }
732 do_action( 'everest_forms_update_entry_status', $entry_id, $status );
733 return array( 'entry_id' => $entry_id, 'status' => $status, 'updated' => (int) $updated );
734 }
735
736 /**
737 * @param array $input Args.
738 * @return array|WP_Error
739 */
740 public static function set_entry_starred( $input ) {
741 return self::set_entry_flag( $input, 'starred' );
742 }
743
744 /**
745 * @param array $input Args.
746 * @return array|WP_Error
747 */
748 public static function set_entry_viewed( $input ) {
749 return self::set_entry_flag( $input, 'viewed' );
750 }
751
752 /**
753 * Shared implementation for boolean entry flags (starred/viewed).
754 *
755 * @param array $input Input args.
756 * @param string $col Column name.
757 * @return array|WP_Error
758 */
759 protected static function set_entry_flag( $input, $col ) {
760 global $wpdb;
761 $entry_id = (int) $input['entry_id'];
762 $value = ! empty( $input[ $col ] ) ? 1 : 0;
763 $updated = $wpdb->update(
764 $wpdb->prefix . 'evf_entries',
765 array( $col => $value ),
766 array( 'entry_id' => $entry_id ),
767 array( '%d' ),
768 array( '%d' )
769 );
770 if ( false === $updated ) {
771 return new WP_Error( 'evf_entry_flag_failed', "Could not update entry {$col}.", array( 'status' => 500 ) );
772 }
773 return array( 'entry_id' => $entry_id, $col => (bool) $value, 'updated' => (int) $updated );
774 }
775
776 /**
777 * @param array $input Args.
778 * @return array
779 */
780 public static function count_entries( $input ) {
781 global $wpdb;
782 $form_id = isset( $input['form_id'] ) ? (int) $input['form_id'] : 0;
783 $table = $wpdb->prefix . 'evf_entries';
784 $where = $form_id > 0 ? $wpdb->prepare( 'WHERE form_id = %d', $form_id ) : '';
785
786 $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} {$where}" );
787 $unread = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} {$where} " . ( '' === $where ? 'WHERE' : 'AND' ) . " viewed = 0 AND status <> 'trash'" );
788 $starred = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} {$where} " . ( '' === $where ? 'WHERE' : 'AND' ) . " starred = 1 AND status <> 'trash'" );
789
790 $by_status = array();
791 foreach ( array( 'publish', 'approved', 'denied', 'pending', 'spam', 'trash' ) as $st ) {
792 $by_status[ $st ] = (int) $wpdb->get_var(
793 $wpdb->prepare( "SELECT COUNT(*) FROM {$table} " . ( $form_id > 0 ? 'WHERE form_id = %d AND' : 'WHERE' ) . " status = %s", $form_id > 0 ? array( $form_id, $st ) : array( $st ) )
794 );
795 }
796
797 return array(
798 'form_id' => $form_id,
799 'total' => $total,
800 'unread' => $unread,
801 'starred' => $starred,
802 'by_status' => $by_status,
803 );
804 }
805
806 /**
807 * @param array $input Args.
808 * @return array|WP_Error
809 */
810 public static function create_entry( $input ) {
811 global $wpdb;
812 $form_id = (int) $input['form_id'];
813 $input_flds = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : array();
814 $status = isset( $input['status'] ) ? sanitize_key( $input['status'] ) : 'publish';
815 $fire_hooks = ! isset( $input['fire_hooks'] ) || ! empty( $input['fire_hooks'] ); // default true
816
817 if ( empty( $input_flds ) ) {
818 return new WP_Error( 'evf_no_fields', 'No fields provided.', array( 'status' => 400 ) );
819 }
820 $post = get_post( $form_id );
821 if ( ! $post || 'everest_form' !== $post->post_type ) {
822 return new WP_Error( 'evf_form_not_found', 'Form not found.', array( 'status' => 404 ) );
823 }
824 if ( ! current_user_can( 'everest_forms_view_form_entries', $form_id ) && ! current_user_can( 'manage_everest_forms' ) ) {
825 return new WP_Error( 'evf_form_forbidden', 'Not allowed to create entries for this form.', array( 'status' => 403 ) );
826 }
827
828 // Decode the form config so we can:
829 // (a) translate caller-friendly meta-key inputs into EVF's rich $fields
830 // shape ({ field_id => { id, name, value, type, meta_key } }) that
831 // upstream listeners (slot booking, email notifications, Zapier,
832 // etc.) require, and
833 // (b) hand the same $form_data array to listeners.
834 $form_data = array();
835 if ( is_string( $post->post_content ) ) {
836 $decoded = json_decode( $post->post_content, true );
837 if ( is_array( $decoded ) ) {
838 $form_data = $decoded;
839 }
840 }
841 $form_fields_config = isset( $form_data['form_fields'] ) && is_array( $form_data['form_fields'] ) ? $form_data['form_fields'] : array();
842
843 // Build a meta-key → field-config index so the caller can refer to
844 // fields by either their generated meta-key ("email_4") or their
845 // human label ("Email") and we figure out the right field-id.
846 $by_meta_key = array();
847 $by_label = array();
848 foreach ( $form_fields_config as $fid => $fcfg ) {
849 if ( ! is_array( $fcfg ) ) {
850 continue;
851 }
852 if ( isset( $fcfg['meta-key'] ) ) {
853 $by_meta_key[ (string) $fcfg['meta-key'] ] = $fid;
854 }
855 if ( isset( $fcfg['label'] ) ) {
856 $by_label[ strtolower( (string) $fcfg['label'] ) ] = $fid;
857 }
858 }
859
860 // Build EVF-shaped $fields ({ field_id => array(...) }) plus a meta-key
861 // map ({ meta_key => string-value }) for the entry row's `fields` blob.
862 $rich_fields = array();
863 $meta_map = array();
864 foreach ( $input_flds as $key => $value ) {
865 $lookup = (string) $key;
866 $fid = isset( $by_meta_key[ $lookup ] ) ? $by_meta_key[ $lookup ] : ( isset( $by_label[ strtolower( $lookup ) ] ) ? $by_label[ strtolower( $lookup ) ] : null );
867
868 $str_val = is_scalar( $value ) ? (string) $value : wp_json_encode( $value );
869
870 if ( null !== $fid && isset( $form_fields_config[ $fid ] ) ) {
871 $fcfg = $form_fields_config[ $fid ];
872 $meta_key = isset( $fcfg['meta-key'] ) ? (string) $fcfg['meta-key'] : sanitize_key( $lookup );
873 $rich_fields[ $fid ] = array(
874 'id' => $fid,
875 'name' => isset( $fcfg['label'] ) ? (string) $fcfg['label'] : '',
876 'value' => $str_val,
877 'value_raw'=> $str_val,
878 'type' => isset( $fcfg['type'] ) ? (string) $fcfg['type'] : '',
879 'meta_key' => $meta_key,
880 );
881 $meta_map[ $meta_key ] = $str_val;
882 } else {
883 // Caller passed a key that doesn't match any field. Persist
884 // the meta row but skip the rich-field hook payload entry
885 // (so downstream listeners that index by field_id don't trip).
886 $meta_key = sanitize_key( $lookup );
887 $meta_map[ $meta_key ] = $str_val;
888 }
889 }
890
891 // EVF stores the rich field shape ({ field_id: {meta_key, value, ...} })
892 // in `evf_entries.fields` as JSON. evf_get_entry($id, true) decodes that
893 // and walks each entry expecting `meta_key`/`value` keys — if we stored
894 // a flat map here, get-entry would return empty fields.
895 $entry_row = array(
896 'user_id' => get_current_user_id(),
897 'user_device' => '',
898 'user_ip_address' => '',
899 'form_id' => $form_id,
900 'referer' => '',
901 'fields' => wp_json_encode( $rich_fields ),
902 'status' => $status,
903 'viewed' => 0,
904 'starred' => 0,
905 'date_created' => gmdate( 'Y-m-d H:i:s' ),
906 );
907
908 $inserted = $wpdb->insert( $wpdb->prefix . 'evf_entries', $entry_row );
909 if ( ! $inserted ) {
910 return new WP_Error( 'evf_entry_create_failed', 'Could not insert entry.', array( 'status' => 500 ) );
911 }
912 $entry_id = (int) $wpdb->insert_id;
913
914 $meta_table = $wpdb->prefix . 'evf_entrymeta';
915 foreach ( $meta_map as $meta_key => $meta_value ) {
916 $wpdb->insert(
917 $meta_table,
918 array(
919 'entry_id' => $entry_id,
920 'meta_key' => $meta_key,
921 'meta_value' => $meta_value,
922 ),
923 array( '%d', '%s', '%s' )
924 );
925 }
926
927 // Fire the full upstream save action so email notifications, Zapier,
928 // Google Sheets, slot-booking, etc. all run normally. Signature must
929 // match: ($entry_id, $fields, $entry, $form_id, $form_data) — passing
930 // 4 args triggers a fatal in EVF_Form_Task::update_slot_booking_value().
931 if ( $fire_hooks ) {
932 $entry_for_action = (object) array(
933 'entry_id' => $entry_id,
934 'form_id' => $form_id,
935 'fields' => $rich_fields,
936 'status' => $status,
937 );
938 do_action( 'everest_forms_complete_entry_save', $entry_id, $rich_fields, $entry_for_action, $form_id, $form_data );
939 }
940
941 return array(
942 'entry_id' => $entry_id,
943 'form_id' => $form_id,
944 'status' => $status,
945 );
946 }
947
948 /**
949 * @param array $input Args.
950 * @return array|WP_Error
951 */
952 public static function update_entry_fields( $input ) {
953 global $wpdb;
954 $entry_id = (int) $input['entry_id'];
955 $fields = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : array();
956 if ( empty( $fields ) ) {
957 return new WP_Error( 'evf_no_fields', 'No fields provided.', array( 'status' => 400 ) );
958 }
959
960 $exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT entry_id FROM {$wpdb->prefix}evf_entries WHERE entry_id = %d", $entry_id ) );
961 if ( ! $exists ) {
962 return new WP_Error( 'evf_entry_not_found', 'Entry not found.', array( 'status' => 404 ) );
963 }
964
965 $meta_table = $wpdb->prefix . 'evf_entrymeta';
966 $changed = array();
967 foreach ( $fields as $field_id => $value ) {
968 $meta_key = sanitize_key( (string) $field_id );
969 $meta_value = is_scalar( $value ) ? (string) $value : wp_json_encode( $value );
970
971 $existing = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM {$meta_table} WHERE entry_id = %d AND meta_key = %s", $entry_id, $meta_key ) );
972 if ( $existing ) {
973 $wpdb->update( $meta_table, array( 'meta_value' => $meta_value ), array( 'meta_id' => (int) $existing ), array( '%s' ), array( '%d' ) );
974 } else {
975 $wpdb->insert( $meta_table, array( 'entry_id' => $entry_id, 'meta_key' => $meta_key, 'meta_value' => $meta_value ), array( '%d', '%s', '%s' ) );
976 }
977 $changed[ $meta_key ] = $meta_value;
978 }
979
980 do_action( 'everest_forms_update_entry_fields', $entry_id, $changed );
981 return array( 'entry_id' => $entry_id, 'updated_fields' => $changed );
982 }
983
984 /* ------------------------------------------------------------------
985 * Analytics
986 * ------------------------------------------------------------------ */
987
988 /**
989 * @param array $input Args.
990 * @return array
991 */
992 public static function analytics_summary( $input ) {
993 global $wpdb;
994 $days = isset( $input['days'] ) ? max( 1, (int) $input['days'] ) : 30;
995 $form_id = isset( $input['form_id'] ) ? (int) $input['form_id'] : 0;
996 $since = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
997
998 $table = $wpdb->prefix . 'evf_entries';
999
1000 $total_forms = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'everest_form' AND post_status = 'publish'" );
1001
1002 if ( $form_id > 0 ) {
1003 $total_entries = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE form_id = %d", $form_id ) );
1004 $recent_entries = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE form_id = %d AND date_created >= %s", $form_id, $since ) );
1005 $top_forms = array();
1006 } else {
1007 $total_entries = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
1008 $recent_entries = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE date_created >= %s", $since ) );
1009
1010 $rows = $wpdb->get_results( $wpdb->prepare(
1011 "SELECT form_id, COUNT(*) AS cnt FROM {$table} WHERE date_created >= %s GROUP BY form_id ORDER BY cnt DESC LIMIT 5",
1012 $since
1013 ) );
1014 $top_forms = array();
1015 foreach ( (array) $rows as $row ) {
1016 $post = get_post( (int) $row->form_id );
1017 $top_forms[] = array(
1018 'form_id' => (int) $row->form_id,
1019 'title' => $post ? $post->post_title : '',
1020 'entries' => (int) $row->cnt,
1021 );
1022 }
1023 }
1024
1025 return array(
1026 'window_days' => $days,
1027 'total_forms' => $total_forms,
1028 'total_entries' => $total_entries,
1029 'recent_entries' => $recent_entries,
1030 'top_forms' => $top_forms,
1031 'scope_form_id' => $form_id,
1032 );
1033 }
1034 }
1035