* @since 1.0.0 */ /** * Class Wpfaevent_Event_Handler * * Handles all event-related AJAX requests. * * @package Wpfaevent * @since 1.0.0 */ class Wpfaevent_Event_Handler { /** * Initialize the class. * * @since 1.0.0 */ public function __construct() { } /** * Handle AJAX request to get event data. * * @since 1.0.0 * @return void */ public function ajax_get_event() { // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON. if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ), ), 403 ); } // Check permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ), 403 ); } $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0; if ( ! $event_id ) { wp_send_json_error( esc_html__( 'Invalid event ID', 'wpfaevent' ) ); } $event = get_post( $event_id ); if ( ! $event || 'wpfa_event' !== $event->post_type ) { wp_send_json_error( esc_html__( 'Event not found', 'wpfaevent' ) ); } $start_time = get_post_meta( $event_id, 'wpfa_event_start_time', true ); if ( '' === $start_time ) { $start_time = get_post_meta( $event_id, 'wpfa_event_time', true ); } $data = array( 'id' => $event_id, 'title' => $event->post_title, 'content' => $event->post_content, 'excerpt' => $event->post_excerpt, 'start_date' => get_post_meta( $event_id, 'wpfa_event_start_date', true ), 'end_date' => get_post_meta( $event_id, 'wpfa_event_end_date', true ), 'start_time' => $start_time, 'end_time' => get_post_meta( $event_id, 'wpfa_event_end_time', true ), 'time' => $start_time, 'timezone' => class_exists( 'Wpfaevent_Meta_Event' ) ? Wpfaevent_Meta_Event::get_event_timezone( $event_id ) : wp_timezone_string(), 'all_day' => class_exists( 'Wpfaevent_Meta_Event' ) ? Wpfaevent_Meta_Event::get_event_all_day( $event_id ) : false, 'location' => get_post_meta( $event_id, 'wpfa_event_location', true ), 'event_url' => get_post_meta( $event_id, 'wpfa_event_url', true ), 'registration_link' => get_post_meta( $event_id, 'wpfa_event_registration_link', true ), 'cfs_link' => get_post_meta( $event_id, 'wpfa_event_cfs_link', true ), 'featured_image' => get_post_thumbnail_id( $event_id ), ); wp_send_json_success( $data ); } /** * Handle AJAX request to add a new event. * * @since 1.0.0 * @return void */ public function ajax_add_event() { // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON. if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ), ), 403 ); } // Check permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wpfaevent' ), ), 403 ); } $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : ''; $excerpt = isset( $_POST['excerpt'] ) ? wp_kses_post( wp_unslash( $_POST['excerpt'] ) ) : ''; $start_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : ''; $location = isset( $_POST['location'] ) ? sanitize_text_field( wp_unslash( $_POST['location'] ) ) : ''; $lead_text = isset( $_POST['lead_text'] ) ? sanitize_text_field( wp_unslash( $_POST['lead_text'] ) ) : ''; $registration_link = isset( $_POST['registration_link'] ) ? esc_url_raw( wp_unslash( $_POST['registration_link'] ) ) : ''; // Validate required fields - must match UI requirements. $required_fields = array( 'title' => $title, 'excerpt' => $this->is_blank_description( $excerpt ) ? '' : $excerpt, 'start_date' => $start_date, 'location' => $location, 'lead_text' => $lead_text, 'registration_link' => $registration_link, ); foreach ( $required_fields as $field_name => $field_value ) { if ( empty( $field_value ) ) { /* translators: %s: Field name */ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field_name ) ); } } // Validate featured image BEFORE creating event (must be required). if ( empty( $_FILES['featured_image']['name'] ) ) { wp_send_json_error( esc_html__( 'Missing required field: featured_image', 'wpfaevent' ) ); } // Validate and upload image before creating event post. $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ); $filename = isset( $_FILES['featured_image']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['featured_image']['name'] ) ) : ''; $tmp_name = isset( $_FILES['featured_image']['tmp_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['featured_image']['tmp_name'] ) ) : ''; $fileinfo = wp_check_filetype_and_ext( $tmp_name, $filename ); $file_type = isset( $fileinfo['type'] ) ? $fileinfo['type'] : ''; if ( empty( $fileinfo['ext'] ) || ! in_array( $file_type, $allowed_types, true ) ) { wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) ); } // Validate file size (2MB max). $max_size = 2 * 1024 * 1024; // 2MB in bytes. $file_size = isset( $_FILES['featured_image']['size'] ) ? intval( $_FILES['featured_image']['size'] ) : 0; if ( $file_size > $max_size ) { wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) ); } // Create event post - now that all validations pass. $event_data = array( 'post_title' => $title, 'post_content' => isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : '', 'post_excerpt' => $excerpt, 'post_type' => 'wpfa_event', 'post_status' => 'publish', ); $event_id = wp_insert_post( $event_data, true ); if ( is_wp_error( $event_id ) ) { wp_send_json_error( $event_id->get_error_message() ); } // Save meta fields - using CORRECT form field names. $meta_fields = array( 'wpfa_event_start_date' => 'start_date', 'wpfa_event_end_date' => 'end_date', 'wpfa_event_location' => 'location', 'wpfa_event_lead_text' => 'lead_text', 'wpfa_event_registration_link' => 'registration_link', 'wpfa_event_cfs_link' => 'cfs_link', ); foreach ( $meta_fields as $meta_key => $post_key ) { if ( isset( $_POST[ $post_key ] ) ) { $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ); // Special handling for URL fields. if ( in_array( $post_key, array( 'registration_link', 'cfs_link' ), true ) ) { $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) ); } if ( strlen( $value ) > 0 ) { update_post_meta( $event_id, $meta_key, $value ); } } } $this->save_event_timing_meta( $event_id, $_POST ); // Handle featured image upload - now safe since validation passed. require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; $attachment_id = media_handle_upload( 'featured_image', $event_id ); if ( is_wp_error( $attachment_id ) ) { // Delete the event if image upload fails. wp_delete_post( $event_id, true ); /* translators: %s: Error message */ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) ); } // Set as featured image. set_post_thumbnail( $event_id, $attachment_id ); wp_send_json_success( array( 'event_id' => $event_id, 'message' => esc_html__( 'Event created successfully!', 'wpfaevent' ), ) ); } /** * Handle AJAX request to update an event. * * @since 1.0.0 * @return void */ public function ajax_update_event() { // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON. if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ), ), 403 ); } // Check permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ), 403 ); } $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0; if ( ! $event_id ) { wp_send_json_error( __( 'Invalid event ID', 'wpfaevent' ) ); } // Verify event exists and user can edit it. $event = get_post( $event_id ); if ( ! $event || 'wpfa_event' !== $event->post_type || ! current_user_can( 'edit_post', $event_id ) ) { wp_send_json_error( __( 'Cannot edit this event', 'wpfaevent' ) ); } // Validate required fields - must match UI requirements. $required_fields = array( 'title', 'excerpt', 'start_date', 'location', 'lead_text', 'registration_link' ); foreach ( $required_fields as $field ) { if ( empty( $_POST[ $field ] ) ) { /* translators: %s: Field name */ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) ); } } // Validate image BEFORE updating event (only if a new image is being uploaded). if ( ! empty( $_FILES['featured_image']['name'] ) ) { $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ); $filename = isset( $_FILES['featured_image']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['featured_image']['name'] ) ) : ''; $tmp_name = isset( $_FILES['featured_image']['tmp_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['featured_image']['tmp_name'] ) ) : ''; $fileinfo = wp_check_filetype_and_ext( $tmp_name, $filename ); $file_type = isset( $fileinfo['type'] ) ? $fileinfo['type'] : ''; if ( empty( $fileinfo['ext'] ) || ! in_array( $file_type, $allowed_types, true ) ) { wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) ); } // Validate file size (2MB max). $max_size = 2 * 1024 * 1024; $file_size = isset( $_FILES['featured_image']['size'] ) ? intval( $_FILES['featured_image']['size'] ) : 0; if ( $file_size > $max_size ) { wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) ); } } // Check and sanitize input fields. $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : ''; $excerpt = isset( $_POST['excerpt'] ) ? wp_kses_post( wp_unslash( $_POST['excerpt'] ) ) : ''; $content = isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : ''; if ( $this->is_blank_description( $excerpt ) ) { /* translators: %s: Field name */ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), 'excerpt' ) ); } // Update post - only after image validation passes. $event_data = array( 'ID' => $event_id, 'post_title' => $title, 'post_excerpt' => $excerpt, ); if ( isset( $_POST['content'] ) ) { $event_data['post_content'] = $content; } $update_result = wp_update_post( $event_data, true ); if ( is_wp_error( $update_result ) || 0 === $update_result ) { wp_send_json_error( array( 'message' => esc_html__( 'Failed to update event.', 'wpfaevent' ), ), 500 ); } // Save meta fields. $meta_fields = array( 'wpfa_event_start_date' => 'start_date', 'wpfa_event_end_date' => 'end_date', 'wpfa_event_location' => 'location', 'wpfa_event_lead_text' => 'lead_text', 'wpfa_event_url' => 'event_url', 'wpfa_event_registration_link' => 'registration_link', 'wpfa_event_cfs_link' => 'cfs_link', ); foreach ( $meta_fields as $meta_key => $post_key ) { if ( isset( $_POST[ $post_key ] ) ) { $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ); if ( in_array( $post_key, array( 'event_url', 'registration_link', 'cfs_link' ), true ) ) { $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) ); } if ( strlen( $value ) === 0 ) { delete_post_meta( $event_id, $meta_key ); } else { update_post_meta( $event_id, $meta_key, $value ); } } } $this->save_event_timing_meta( $event_id, $_POST ); // Handle featured image upload - now safe since validation passed. if ( ! empty( $_FILES['featured_image']['name'] ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; // Upload and create attachment. $attachment_id = media_handle_upload( 'featured_image', $event_id ); if ( is_wp_error( $attachment_id ) ) { /* translators: %s: Error message */ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) ); } // Set as featured image. set_post_thumbnail( $event_id, $attachment_id ); } elseif ( isset( $_POST['remove_featured_image'] ) && 'true' === $_POST['remove_featured_image'] ) { // Remove featured image. delete_post_thumbnail( $event_id ); } wp_send_json_success(); } /** * Save front-end event timing fields into calendar-aware event meta. * * @since 1.0.0 * * @param int $event_id Event post ID. * @param array $request Request data. * @return void * @phpstan-param array $request */ private function save_event_timing_meta( $event_id, $request ) { $event_id = absint( $event_id ); if ( ! $event_id || ! is_array( $request ) ) { return; } $has_timing_payload = isset( $request['timezone'] ) || isset( $request['all_day'] ) || isset( $request['start_time'] ) || isset( $request['end_time'] ) || isset( $request['time'] ); if ( ! $has_timing_payload ) { $this->sync_event_datetime_meta( $event_id ); return; } if ( isset( $request['timezone'] ) ) { $posted_timezone = sanitize_text_field( wp_unslash( $request['timezone'] ) ); $timezone = class_exists( 'Wpfaevent_Meta_Event' ) ? Wpfaevent_Meta_Event::sanitize_timezone( $posted_timezone ) : $posted_timezone; $this->update_or_delete_post_meta( $event_id, 'wpfa_event_timezone', $timezone ); } $posted_all_day = isset( $request['all_day'] ) ? wp_unslash( $request['all_day'] ) : false; $all_day = is_bool( $posted_all_day ) || is_string( $posted_all_day ) || is_int( $posted_all_day ) ? rest_sanitize_boolean( $posted_all_day ) : (bool) $posted_all_day; update_post_meta( $event_id, 'wpfa_event_all_day', $all_day ? '1' : '0' ); $posted_start_time = isset( $request['start_time'] ) ? sanitize_text_field( wp_unslash( $request['start_time'] ) ) : ''; $posted_end_time = isset( $request['end_time'] ) ? sanitize_text_field( wp_unslash( $request['end_time'] ) ) : ''; if ( '' === $posted_start_time && isset( $request['time'] ) ) { $posted_start_time = sanitize_text_field( wp_unslash( $request['time'] ) ); } $start_time = class_exists( 'Wpfaevent_Meta_Event' ) ? Wpfaevent_Meta_Event::sanitize_time_value( $posted_start_time ) : $posted_start_time; $end_time = class_exists( 'Wpfaevent_Meta_Event' ) ? Wpfaevent_Meta_Event::sanitize_time_value( $posted_end_time ) : $posted_end_time; if ( $all_day ) { delete_post_meta( $event_id, 'wpfa_event_start_time' ); delete_post_meta( $event_id, 'wpfa_event_time' ); delete_post_meta( $event_id, 'wpfa_event_end_time' ); delete_post_meta( $event_id, 'wpfa_event_starts_at' ); delete_post_meta( $event_id, 'wpfa_event_ends_at' ); return; } $this->update_or_delete_post_meta( $event_id, 'wpfa_event_start_time', $start_time ); $this->update_or_delete_post_meta( $event_id, 'wpfa_event_time', $start_time ); $this->update_or_delete_post_meta( $event_id, 'wpfa_event_end_time', $end_time ); $this->sync_event_datetime_meta( $event_id ); } /** * Keep calendar-aware datetime meta in sync after front-end AJAX saves. * * @since 1.0.0 * * @param int $event_id Event post ID. * @return void */ private function sync_event_datetime_meta( $event_id ) { $event_id = absint( $event_id ); if ( ! $event_id || ! class_exists( 'Wpfaevent_Meta_Event' ) ) { return; } if ( Wpfaevent_Meta_Event::get_event_all_day( $event_id ) ) { delete_post_meta( $event_id, 'wpfa_event_starts_at' ); delete_post_meta( $event_id, 'wpfa_event_ends_at' ); return; } $start_date = Wpfaevent_Meta_Event::sanitize_date_value( get_post_meta( $event_id, 'wpfa_event_start_date', true ) ); $end_date = Wpfaevent_Meta_Event::sanitize_date_value( get_post_meta( $event_id, 'wpfa_event_end_date', true ) ); $start_time = Wpfaevent_Meta_Event::sanitize_time_value( get_post_meta( $event_id, 'wpfa_event_start_time', true ) ); $end_time = Wpfaevent_Meta_Event::sanitize_time_value( get_post_meta( $event_id, 'wpfa_event_end_time', true ) ); $timezone = Wpfaevent_Meta_Event::get_event_timezone( $event_id ); if ( '' === $start_time ) { $start_time = Wpfaevent_Meta_Event::sanitize_time_value( get_post_meta( $event_id, 'wpfa_event_time', true ) ); if ( '' !== $start_time ) { update_post_meta( $event_id, 'wpfa_event_start_time', $start_time ); } } $this->update_or_delete_post_meta( $event_id, 'wpfa_event_time', $start_time ); $this->update_or_delete_post_meta( $event_id, 'wpfa_event_starts_at', Wpfaevent_Meta_Event::build_datetime_value( $start_date, $start_time, $timezone ) ); $this->update_or_delete_post_meta( $event_id, 'wpfa_event_ends_at', Wpfaevent_Meta_Event::build_datetime_value( '' !== $end_date ? $end_date : $start_date, $end_time, $timezone ) ); } /** * Update a meta value or delete it when empty. * * @since 1.0.0 * * @param int $post_id Post ID. * @param string $key Meta key. * @param string $value Meta value. * @return void */ private function update_or_delete_post_meta( $post_id, $key, $value ) { if ( '' === $value ) { delete_post_meta( $post_id, $key ); return; } update_post_meta( $post_id, $key, $value ); } /** * Check whether a description has no visible text. * * The visual editor can send markup such as `

 

` for a field * that looks empty, so tags, entities and whitespace are ignored. * * @since 1.0.0 * * @param string $description Description HTML. * @return bool True when nothing would be shown. */ private function is_blank_description( $description ) { $text = html_entity_decode( wp_strip_all_tags( $description ), ENT_QUOTES, 'UTF-8' ); return '' === (string) preg_replace( '/[\s\x{00A0}]+/u', '', $text ); } /** * Handle AJAX request to delete an event. * * @since 1.0.0 * @return void */ public function ajax_delete_event() { // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON. if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ), ), 403 ); } // Check permissions. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ), 403 ); } $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0; if ( ! $event_id ) { wp_send_json_error( __( 'Invalid event ID', 'wpfaevent' ) ); } // Verify event exists and user can delete it. $event = get_post( $event_id ); if ( ! $event || 'wpfa_event' !== $event->post_type || ! current_user_can( 'delete_post', $event_id ) ) { wp_send_json_error( __( 'Cannot delete this event', 'wpfaevent' ) ); } // Delete the event. $result = wp_delete_post( $event_id, true ); if ( ! $result ) { wp_send_json_error( __( 'Failed to delete event', 'wpfaevent' ) ); } wp_send_json_success(); } }