← All changes
|
includes/page-appointment-services/ajax/appointment_services__list.php
+127
-66
11.5
→
11.9
View file →
| @@ -1,80 +1,141 @@ | ||
| 1 | 1 | <?php |
| 2 | -/** AJAX: list Appointment Services. @package Booking Calendar */ | |
| 3 | -if ( ! defined( 'ABSPATH' ) ) { exit; } | |
| 2 | +/** | |
| 3 | + * AJAX list endpoint for the template-driven Appointment Services catalog. | |
| 4 | + * | |
| 5 | + * @package Booking Calendar | |
| 6 | + * @since 11.6.0 | |
| 7 | + */ | |
| 4 | 8 | |
| 9 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 10 | + exit; | |
| 11 | +} | |
| 12 | + | |
| 5 | 13 | /** |
| 6 | - * Return the filtered Service list and Provider presentation directory. | |
| 14 | + * Return a safe request sequence from an untrusted payload. | |
| 7 | 15 | * |
| 8 | - * Status counts are calculated from the complete provider-filtered result so | |
| 9 | - * the management table can switch status without a separate count request. | |
| 16 | + * @param mixed $request_values Untrusted request values. | |
| 10 | 17 | * |
| 11 | - * @return void Terminates with a JSON success or error response. | |
| 18 | + * @return int Non-negative request sequence or zero. | |
| 12 | 19 | */ |
| 20 | +function wpbc_appointment_services_get_catalog_request_id( $request_values ) { | |
| 21 | + if ( ! is_array( $request_values ) || ! isset( $request_values['request_id'] ) || ! is_scalar( $request_values['request_id'] ) ) { | |
| 22 | + return 0; | |
| 23 | + } | |
| 24 | + | |
| 25 | + return preg_match( '/^\d+$/', (string) $request_values['request_id'] ) ? (int) $request_values['request_id'] : 0; | |
| 26 | +} | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * Send a normalized Services catalog error. | |
| 30 | + * | |
| 31 | + * @param int $request_id Client request sequence. | |
| 32 | + * @param WP_Error $error Safe error. | |
| 33 | + * @param int $status HTTP status. | |
| 34 | + * @param bool $retryable Whether the browser may retry. | |
| 35 | + * | |
| 36 | + * @return void Terminates the AJAX request. | |
| 37 | + */ | |
| 38 | +function wpbc_appointment_services_send_catalog_error( $request_id, $error, $status, $retryable = false ) { | |
| 39 | + wp_send_json( | |
| 40 | + WPBC_UI_Catalog_Response::from_wp_error( 'appointment_services_catalog', $request_id, $error, $retryable ), | |
| 41 | + absint( $status ) | |
| 42 | + ); | |
| 43 | +} | |
| 44 | + | |
| 45 | +/** | |
| 46 | + * Serve the authorized Service list through the shared catalog contract. | |
| 47 | + * | |
| 48 | + * Transport authorization and request normalization stay here. The Service | |
| 49 | + * repository owns SQL and ownership, while the DTO owns the item contract. | |
| 50 | + * | |
| 51 | + * @return void Terminates the AJAX request. | |
| 52 | + */ | |
| 13 | 53 | function wpbc_appointment_services_ajax_list() { |
| 14 | - wpbc_appointment_services_ajax_authorize(); | |
| 15 | - $service_listing = wpbc_appointment_services_get_catalog_listing(); | |
| 16 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; the flag only enables an allow-listed user preference write. | |
| 17 | - $save_items_per_page = isset( $_POST['save_items_per_page'] ) | |
| 18 | - && is_scalar( $_POST['save_items_per_page'] ) | |
| 19 | - && '1' === sanitize_text_field( wp_unslash( $_POST['save_items_per_page'] ) ); | |
| 20 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above and normalized by WPBC_UI_Listing. | |
| 21 | - if ( $save_items_per_page && isset( $_POST['items_per_page'] ) ) { | |
| 22 | - $service_listing->save_items_per_page( wp_unslash( $_POST['items_per_page'] ) ); | |
| 54 | + $configuration = WPBC_UI_Catalog_Registry::get_instance()->get_configuration( 'appointment_services_catalog' ); | |
| 55 | + if ( empty( $configuration ) ) { | |
| 56 | + wpbc_appointment_services_send_catalog_error( 0, new WP_Error( 'wpbc_appointment_services_unavailable', __( 'The Services catalog is unavailable.', 'booking' ) ), 503, true ); | |
| 23 | 57 | } |
| 24 | - $provider = wpbc_appointment_services_get_data_provider(); | |
| 25 | - if ( ! is_object( $provider ) || ! method_exists( $provider, 'list_items' ) || ! wpbc_appointment_services_storage_is_ready() ) { | |
| 26 | - wp_send_json_success( | |
| 58 | + | |
| 59 | + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified immediately below. | |
| 60 | + $raw_request = is_array( $_POST ) ? wp_unslash( $_POST ) : array(); | |
| 61 | + $request_id = wpbc_appointment_services_get_catalog_request_id( $raw_request ); | |
| 62 | + if ( false === check_ajax_referer( $configuration['nonce_name'], 'nonce', false ) ) { | |
| 63 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_invalid_nonce', __( 'Security check failed.', 'booking' ) ), 403 ); | |
| 64 | + } | |
| 65 | + if ( ! current_user_can( wpbc_appointment_services_get_manage_capability() ) ) { | |
| 66 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_forbidden', __( 'You do not have permission to view Services.', 'booking' ) ), 403 ); | |
| 67 | + } | |
| 68 | + | |
| 69 | + $preference_action = isset( $raw_request['preference_action'] ) && is_scalar( $raw_request['preference_action'] ) ? sanitize_key( (string) $raw_request['preference_action'] ) : ''; | |
| 70 | + if ( ! in_array( $preference_action, array( '', 'save', 'reset' ), true ) ) { | |
| 71 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_invalid_preferences', __( 'The catalog preference request is invalid.', 'booking' ) ), 400 ); | |
| 72 | + } | |
| 73 | + $preference_revision = isset( $raw_request['preference_revision'] ) && is_scalar( $raw_request['preference_revision'] ) && preg_match( '/^\d+$/', (string) $raw_request['preference_revision'] ) | |
| 74 | + ? (string) $raw_request['preference_revision'] | |
| 75 | + : '0'; | |
| 76 | + if ( isset( $raw_request['preferences_only'] ) && ( ! is_scalar( $raw_request['preferences_only'] ) || ! in_array( (string) $raw_request['preferences_only'], array( '0', '1' ), true ) ) ) { | |
| 77 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_invalid_preferences', __( 'The catalog preference request is invalid.', 'booking' ) ), 400 ); | |
| 78 | + } | |
| 79 | + $preferences_only = isset( $raw_request['preferences_only'] ) && '1' === (string) $raw_request['preferences_only']; | |
| 80 | + if ( '' !== $preference_action && '0' === $preference_revision ) { | |
| 81 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_invalid_preferences', __( 'The catalog preference revision is invalid.', 'booking' ) ), 400 ); | |
| 82 | + } | |
| 83 | + if ( $preferences_only && 'save' !== $preference_action ) { | |
| 84 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_invalid_preferences', __( 'The catalog preference request is invalid.', 'booking' ) ), 400 ); | |
| 85 | + } | |
| 86 | + if ( 'reset' === $preference_action && ! WPBC_UI_Catalog_Preferences::reset( 'appointment_services_catalog', 0, $preference_revision ) ) { | |
| 87 | + wpbc_appointment_services_send_catalog_error( $request_id, new WP_Error( 'wpbc_appointment_services_preference_reset_failed', __( 'The catalog preferences could not be reset.', 'booking' ) ), 500, true ); | |
| 88 | + } | |
| 89 | + | |
| 90 | + $stored_preferences = WPBC_UI_Catalog_Preferences::load( 'appointment_services_catalog' ); | |
| 91 | + $shared_keys = array( 'request_id', 'page_number', 'items_per_page', 'sort_by', 'sort_order', 'search', 'visible_columns', 'column_order', 'template_pack' ); | |
| 92 | + $shared_request = WPBC_UI_Catalog_Request::create( | |
| 93 | + $configuration, | |
| 94 | + array_intersect_key( $raw_request, array_fill_keys( $shared_keys, true ) ), | |
| 95 | + $stored_preferences | |
| 96 | + ); | |
| 97 | + if ( is_wp_error( $shared_request ) ) { | |
| 98 | + wpbc_appointment_services_send_catalog_error( $request_id, $shared_request, 400 ); | |
| 99 | + } | |
| 100 | + | |
| 101 | + $service_values = array( | |
| 102 | + 'status' => isset( $stored_preferences['status'] ) ? $stored_preferences['status'] : 'all', | |
| 103 | + 'resource_id' => isset( $stored_preferences['resource_id'] ) ? $stored_preferences['resource_id'] : 0, | |
| 104 | + ); | |
| 105 | + foreach ( array( 'status', 'resource_id' ) as $service_key ) { | |
| 106 | + if ( array_key_exists( $service_key, $raw_request ) ) { | |
| 107 | + $service_values[ $service_key ] = $raw_request[ $service_key ]; | |
| 108 | + } | |
| 109 | + } | |
| 110 | + $service_request = WPBC_Appointment_Services_Catalog_Request::create( $service_values ); | |
| 111 | + if ( is_wp_error( $service_request ) ) { | |
| 112 | + wpbc_appointment_services_send_catalog_error( $request_id, $service_request, 400 ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + if ( 'save' === $preference_action ) { | |
| 116 | + $preference_result = WPBC_UI_Catalog_Preferences::save( | |
| 117 | + 'appointment_services_catalog', | |
| 118 | + $shared_request, | |
| 27 | 119 | array( |
| 28 | - 'storage_ready' => false, | |
| 29 | - 'services' => array(), | |
| 30 | - 'listing' => $service_listing->get_client_settings(), | |
| 31 | - 'message' => wpbc_appointment_services_storage_error()->get_error_message(), | |
| 32 | - ) | |
| 120 | + 'status' => $service_request->get( 'status', 'all' ), | |
| 121 | + 'resource_id' => $service_request->get( 'resource_id', 0 ), | |
| 122 | + ), | |
| 123 | + 0, | |
| 124 | + $preference_revision | |
| 33 | 125 | ); |
| 126 | + if ( is_wp_error( $preference_result ) ) { | |
| 127 | + wpbc_appointment_services_send_catalog_error( $request_id, $preference_result, 400 ); | |
| 128 | + } | |
| 34 | 129 | } |
| 35 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; sanitized before the repository boundary. | |
| 36 | - $search = isset( $_POST['search'] ) && is_scalar( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; | |
| 37 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; validated against the status allow-list. | |
| 38 | - $status = isset( $_POST['status'] ) && is_scalar( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'active'; | |
| 39 | - if ( ! in_array( $status, array( 'all', 'active', 'inactive', 'archived' ), true ) ) { $status = 'active'; } | |
| 40 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; normalized to a positive integer. | |
| 41 | - $resource_id = isset( $_POST['resource_id'] ) && is_scalar( $_POST['resource_id'] ) ? absint( $_POST['resource_id'] ) : 0; | |
| 42 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; normalized by the shared listing component. | |
| 43 | - $page_number = isset( $_POST['page_number'] ) ? wp_unslash( $_POST['page_number'] ) : 1; | |
| 44 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; normalized against the configured allow-list. | |
| 45 | - $items_per_page = isset( $_POST['items_per_page'] ) ? wp_unslash( $_POST['items_per_page'] ) : $service_listing->get_items_per_page(); | |
| 46 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; normalized against sortable listing columns. | |
| 47 | - $sort_by = isset( $_POST['sort_by'] ) ? wp_unslash( $_POST['sort_by'] ) : null; | |
| 48 | - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Authorized above; normalized to asc or desc. | |
| 49 | - $sort_order = isset( $_POST['sort_order'] ) ? wp_unslash( $_POST['sort_order'] ) : null; | |
| 50 | - $catalog_page = wpbc_appointment_services_get_catalog_page( | |
| 51 | - $provider, | |
| 52 | - array( | |
| 53 | - 'search' => $search, | |
| 54 | - 'status' => $status, | |
| 55 | - 'resource_id' => $resource_id, | |
| 56 | - 'page_number' => $page_number, | |
| 57 | - 'items_per_page' => $items_per_page, | |
| 58 | - 'sort_by' => $sort_by, | |
| 59 | - 'sort_order' => $sort_order, | |
| 60 | - ), | |
| 61 | - $service_listing | |
| 62 | - ); | |
| 63 | - if ( is_wp_error( $catalog_page ) ) { | |
| 64 | - wpbc_appointment_services_send_provider_error( $catalog_page, __( 'Services could not be loaded.', 'booking' ) ); | |
| 130 | + if ( $preferences_only ) { | |
| 131 | + wp_send_json( array( 'success' => true, 'request_id' => $request_id ), 200 ); | |
| 65 | 132 | } |
| 66 | - $directory = wpbc_appointment_services_get_provider_directory(); | |
| 67 | - wp_send_json_success( | |
| 68 | - array( | |
| 69 | - 'storage_ready' => true, | |
| 70 | - 'services' => $catalog_page['services'], | |
| 71 | - 'counts' => $catalog_page['counts'], | |
| 72 | - 'pagination' => $catalog_page['pagination'], | |
| 73 | - 'sorting' => $catalog_page['sorting'], | |
| 74 | - 'providers' => array_values( $directory ), | |
| 75 | - 'provider_count'=> count( $directory ), | |
| 76 | - 'listing' => $service_listing->get_client_settings(), | |
| 77 | - ) | |
| 78 | - ); | |
| 133 | + | |
| 134 | + $response = ( new WPBC_Appointment_Services_Catalog_Provider( wpbc_appointment_services_get_data_provider(), null, $service_request ) )->get_response( $shared_request ); | |
| 135 | + if ( is_wp_error( $response ) ) { | |
| 136 | + wpbc_appointment_services_send_catalog_error( $request_id, $response, 500, true ); | |
| 137 | + } | |
| 138 | + | |
| 139 | + wp_send_json( $response->to_array(), 200 ); | |
| 79 | 140 | } |
| 80 | 141 | add_action( 'wp_ajax_WPBC_AJX_APPOINTMENT_SERVICES_LIST', 'wpbc_appointment_services_ajax_list' ); |