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 |