'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => self::defaults(), 'show_in_rest' => false, ) ); } /** * Saves settings from Abientot admin forms. */ /** Form-configuration fields outside the settings array that extensions save on abientot_before_settings_save. */ public const EXTENSION_FIELDS = array( 'abientot_resource_form_present', 'abientot_resource_form_category', 'abientot_resource_form_show_usage_time', 'abientot_waitlist_form_present', 'abientot_waitlist_form_enabled' ); /** * Tab context of the settings form being saved, set by save() after nonce verification. * Other option updates (imports, upgrades) sanitize without it. * * @var array{active_tab:string,full_form:bool}|null */ private static ?array $form_context = null; public function save(): void { check_admin_referer( 'abientot_save_settings' ); if ( ! current_user_can( 'abientot_manage_settings' ) ) { wp_die( esc_html__( 'You do not have permission to manage Abientot settings.', 'abientot' ), 403 ); } BookingLock::require_configuration_lock(); $input = \Abientot\Support\Input::post( 'abientot_save_settings' ); $previous_settings = self::values(); $form_id = $input->has( 'abientot_form_id' ) ? \Abientot\Support\BookingForms::resolve( $input->text( 'abientot_form_id' ) ) : 0; $name = $input->has( 'abientot_form_name' ) && ! $input->has_list( 'abientot_form_name' ) ? $input->text( 'abientot_form_name' ) : null; self::$form_context = array( 'active_tab' => $input->key( 'abientot_settings_active_tab', 'general' ), 'full_form' => $input->filled( 'abientot_settings_full_form' ), ); // An additional booking form is sanitized on top of that form's settings and stored on the form only. $filter = null; if ( $form_id > 0 ) { $base = \Abientot\Support\BookingForms::settings( $form_id ); $filter = static function () use ( $base ) { return $base; }; add_filter( 'option_' . self::OPTION_NAME, $filter, 1000 ); } if ( ! has_filter( 'sanitize_option_' . self::OPTION_NAME ) ) { $this->register(); } try { // sanitize_option() runs the sanitize_callback registered in register(): every field gets its own sanitizer. $sanitized = sanitize_option( self::OPTION_NAME, isset( $_POST[ self::OPTION_NAME ] ) ? wp_unslash( $_POST[ self::OPTION_NAME ] ) : array() ); } finally { if ( null !== $filter ) { remove_filter( 'option_' . self::OPTION_NAME, $filter, 1000 ); } } $sanitized = is_array( $sanitized ) ? $sanitized : array(); do_action( 'abientot_before_settings_save', $sanitized, $input->fields( self::EXTENSION_FIELDS ) ); if ( $form_id > 0 ) { \Abientot\Support\BookingForms::save( $form_id, $sanitized, $name ); wp_safe_redirect( add_query_arg( array( 'page' => 'abientot-form', 'form' => $form_id, 'settings-updated' => 'true' ), admin_url( 'admin.php' ) ) ); exit; } if ( null !== $name ) { \Abientot\Support\BookingForms::rename( 0, $name ); } update_option( self::OPTION_NAME, $sanitized, false ); if ( ! get_settings_errors( self::OPTION_NAME ) ) { add_settings_error( self::OPTION_NAME, 'abientot_settings_saved', __( 'Settings saved.', 'abientot' ), 'updated' ); } $this->sync_service_currency( (string) $previous_settings['currency'], (string) self::values()['currency'] ); set_transient( 'settings_errors', get_settings_errors(), 30 ); wp_safe_redirect( $this->settings_redirect_url( $input->key( 'abientot_settings_return_page' ) ) ); exit; } /** * Returns default settings for the free plugin. * * @return array */ public static function defaults(): array { return array( 'plugin_language' => '', 'business_name' => get_bloginfo( 'name' ), 'header_name_font' => 'serif', 'header_name_style' => 'bold', 'header_name_size' => 'large', 'header_name_color' => '#131f29', 'business_timezone' => wp_timezone_string(), 'date_display_format' => 'wordpress', 'time_display_format' => 'wordpress', 'currency' => 'USD', 'booking_min_notice_hours' => 24, 'booking_window_days' => 90, 'booking_slot_interval_minutes' => 30, 'default_booking_status' => 'pending', 'match_customer_by_email' => true, 'match_customer_by_phone' => true, 'cancellation_cutoff_hours' => 24, 'rescheduling_cutoff_hours' => 24, 'admin_notification_email' => get_option( 'admin_email' ), 'email_from_name' => get_bloginfo( 'name' ), 'email_from_address' => get_option( 'admin_email' ), 'email_reply_to_address' => get_option( 'admin_email' ), 'email_customer_confirmation_enabled' => true, 'email_customer_confirmed_enabled' => true, 'email_admin_new_booking_enabled' => true, 'email_customer_cancellation_enabled' => false, 'email_customer_rescheduling_enabled' => false, 'email_customer_completed_enabled' => false, 'email_show_credit' => false, 'form_primary_color' => '#46c8b2', 'form_background_color' => '#ffffff', 'form_background_opacity' => 100, 'form_text_color' => '#131f29', 'form_font_family' => 'system', 'form_density' => 'compact', 'form_font_size' => 15, 'form_auto_text_color' => false, 'form_shadow' => 'soft', 'form_look' => '', 'form_max_width' => 760, 'form_padding' => 24, 'form_container_border_radius' => 24, 'form_border_radius' => 16, 'form_hours_border_radius' => 12, 'form_border_width' => 1, 'form_border_color' => '#d6d9dd', 'form_service_border_width' => 1, 'form_service_border_color_mode' => 'custom', 'form_service_border_color' => '#d6d9dd', 'form_service_background_color_mode' => 'custom', 'form_service_background_color' => '#ffffff', 'form_category_background_color' => '#ffffff', 'form_field_text_color' => '#1f2933', 'form_field_background_color' => '#ffffff', 'form_field_border_color' => '#d6d9dd', 'form_field_border_radius' => 8, 'form_field_border_width' => 1, 'form_service_layout' => 'list', 'form_group_services' => false, 'form_category_behavior' => 'collapsed', 'form_show_service_images' => false, 'form_show_category_images' => false, 'form_image_size' => 72, 'form_image_radius' => 6, 'form_image_border_width' => 1, 'form_image_fit' => 'cover', 'form_show_map' => false, 'form_map_height' => 220, 'form_title' => __( 'Book an appointment', 'abientot' ), 'form_intro' => __( 'Choose a service, pick an available time, and send your booking request.', 'abientot' ), 'form_button_label' => __( 'Book now', 'abientot' ), 'form_display_mode' => 'embedded', 'confirmation_message' => __( 'Thank you. Your booking request has been received.', 'abientot' ), 'form_fields' => self::default_form_fields(), 'require_policy_checkbox' => false, 'booking_terms' => '', 'policy_checkbox_text' => __( 'I agree to the booking terms.', 'abientot' ), 'privacy_controller_name' => '', 'privacy_contact' => '', 'privacy_policy_url' => '', 'privacy_retention_statement' => '', 'privacy_recipients' => '', 'default_payment_status' => 'unpaid', 'pay_on_site_enabled' => true, 'payment_instructions' => __( 'Please pay on site at your appointment.', 'abientot' ), 'delete_data_on_uninstall' => false, 'debug_mode' => false, ) + self::email_template_defaults(); } /** * Returns settings merged with current defaults. * * @return array */ /** * Choices for the business name shown in the admin header. * * @return array> */ public static function header_name_choices(): array { return array( 'font' => array( 'system' => __( 'Modern system', 'abientot' ), 'serif' => __( 'Editorial serif', 'abientot' ), 'rounded' => __( 'Friendly rounded', 'abientot' ), 'mono' => __( 'Technical mono', 'abientot' ), 'script' => __( 'Elegant script', 'abientot' ), ), 'style' => array( 'bold' => __( 'Bold', 'abientot' ), 'italic' => __( 'Elegant italic', 'abientot' ), 'caps' => __( 'Spaced capitals', 'abientot' ), 'light' => __( 'Light', 'abientot' ), ), 'size' => array( 'medium' => __( 'Medium', 'abientot' ), 'large' => __( 'Large', 'abientot' ), 'xlarge' => __( 'Extra large', 'abientot' ), ), ); } /** * @param array $input Raw input. * @param array $settings Settings being saved. * @return array */ private static function sanitize_header_name( array $input, array $settings ): array { $defaults = self::defaults(); foreach ( self::header_name_choices() as $part => $choices ) { $key = 'header_name_' . $part; if ( array_key_exists( $key, $input ) ) { $value = is_string( $input[ $key ] ) ? $input[ $key ] : ''; $settings[ $key ] = isset( $choices[ $value ] ) ? $value : $defaults[ $key ]; } } if ( array_key_exists( 'header_name_color', $input ) ) { $settings['header_name_color'] = sanitize_hex_color( is_string( $input['header_name_color'] ) ? $input['header_name_color'] : '' ) ?: $defaults['header_name_color']; } return $settings; } public static function values(): array { $settings = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } foreach ( EmailService::legacy_template_body_defaults() as $template_key => $legacy_body ) { $body_key = 'email_' . $template_key . '_body'; if ( isset( $settings[ $body_key ] ) && $legacy_body === $settings[ $body_key ] ) { $settings[ $body_key ] = EmailService::template_body_defaults()[ $template_key ]; } } return array_merge( self::defaults(), $settings ); } /** * Returns the default public booking detail fields. * * @return array> */ public static function default_form_fields(): array { return array( array( 'key' => 'first_name', 'label' => __( 'First name', 'abientot' ), 'type' => 'text', 'required' => false ), array( 'key' => 'last_name', 'label' => __( 'Last name', 'abientot' ), 'type' => 'text', 'required' => false ), array( 'key' => 'email', 'label' => __( 'Email', 'abientot' ), 'type' => 'email', 'required' => true ), array( 'key' => 'phone', 'label' => __( 'Phone', 'abientot' ), 'type' => 'tel', 'required' => false ), array( 'key' => 'customer_note', 'label' => __( 'Note', 'abientot' ), 'type' => 'textarea', 'required' => false ), ); } /** * Returns saved public booking detail fields with stable keys and safe labels. * * @param mixed $fields Raw field list. * @return array> */ public static function normalize_form_fields( $fields ): array { if ( ! is_array( $fields ) ) { return self::default_form_fields(); } $normalized = array(); $seen = array(); $types = array( 'text', 'email', 'tel', 'textarea', 'checkbox' ); foreach ( $fields as $index => $field ) { if ( ! is_array( $field ) ) { continue; } $key = isset( $field['key'] ) ? sanitize_key( (string) $field['key'] ) : ''; if ( '' === $key || '_abientot_policy' === $key ) { $key = 'custom_' . absint( $index ); } if ( isset( $seen[ $key ] ) ) { $key .= '_' . absint( $index ); } $label = isset( $field['label'] ) ? sanitize_text_field( (string) $field['label'] ) : ''; if ( '' === $label ) { continue; } $type = isset( $field['type'] ) ? sanitize_key( (string) $field['type'] ) : 'text'; if ( ! in_array( $type, $types, true ) ) { $type = 'text'; } $seen[ $key ] = true; $normalized[] = array( 'key' => $key, 'label' => $label, 'type' => $type, 'required' => ! empty( $field['required'] ), ); if ( count( $normalized ) >= 30 ) { break; } } return $normalized ? $normalized : self::default_form_fields(); } /** * Sanitizes settings before saving. * * @param mixed $input Raw option input. * @return array */ public function sanitize( $input ): array { BookingLock::require_configuration_lock(); $previous = self::values(); $input = is_array( $input ) ? $input : array(); $active_tab = self::$form_context['active_tab'] ?? 'general'; $is_full_form = ! empty( self::$form_context['full_form'] ); $settings = $previous; if ( array_key_exists( 'plugin_language', $input ) ) { $settings['plugin_language'] = $this->sanitize_language( (string) $input['plugin_language'] ); } if ( array_key_exists( 'business_name', $input ) ) { $settings['business_name'] = sanitize_text_field( $input['business_name'] ); } $settings = self::sanitize_header_name( $input, $settings ); foreach ( array( 'privacy_controller_name', 'privacy_contact', 'privacy_retention_statement', 'privacy_recipients' ) as $key ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = sanitize_textarea_field( is_string( $input[ $key ] ) ? $input[ $key ] : '' ); } } if ( array_key_exists( 'privacy_policy_url', $input ) ) { $settings['privacy_policy_url'] = esc_url_raw( is_string( $input['privacy_policy_url'] ) ? $input['privacy_policy_url'] : '', array( 'http', 'https' ) ); } if ( array_key_exists( 'business_timezone', $input ) ) { $settings['business_timezone'] = $this->sanitize_timezone( $input['business_timezone'] ); } if ( array_key_exists( 'date_display_format', $input ) ) { $settings['date_display_format'] = $this->sanitize_choice( (string) $input['date_display_format'], array_keys( DateTimeHelper::date_format_choices() ), 'wordpress' ); } if ( array_key_exists( 'time_display_format', $input ) ) { $settings['time_display_format'] = $this->sanitize_choice( (string) $input['time_display_format'], array_keys( DateTimeHelper::time_format_choices() ), 'wordpress' ); } if ( array_key_exists( 'currency', $input ) ) { $settings['currency'] = $this->sanitize_currency( $input['currency'] ); if ( $settings['currency'] !== self::values()['currency'] && ! $this->currency_prices_compatible( $settings['currency'] ) ) { add_settings_error( self::OPTION_NAME, 'currency_precision', __( 'Some saved service or option prices contain fractions. Adjust those prices before choosing a whole-unit currency.', 'abientot' ), 'error' ); return self::values(); } } if ( array_key_exists( 'booking_min_notice_hours', $input ) ) { $settings['booking_min_notice_hours'] = $this->sanitize_integer( $input['booking_min_notice_hours'], 0, 8760 ); } if ( array_key_exists( 'booking_window_days', $input ) ) { $settings['booking_window_days'] = $this->sanitize_integer( $input['booking_window_days'], 1, 730 ); } if ( array_key_exists( 'booking_slot_interval_minutes', $input ) ) { $settings['booking_slot_interval_minutes'] = $this->sanitize_integer( $input['booking_slot_interval_minutes'], 5, 240 ); } if ( array_key_exists( 'default_booking_status', $input ) ) { $settings['default_booking_status'] = $this->sanitize_choice( $input['default_booking_status'], array( 'pending', 'confirmed' ), 'pending' ); } if ( ! empty( $input['status_labels'] ) && is_array( $input['status_labels'] ) ) { $this->sanitize_status_labels( $input['status_labels'] ); } if ( $is_full_form || 'customer-identity' === $active_tab ) { $settings['match_customer_by_email'] = ! empty( $input['match_customer_by_email'] ); $settings['match_customer_by_phone'] = ! empty( $input['match_customer_by_phone'] ); } if ( array_key_exists( 'cancellation_cutoff_hours', $input ) ) { $settings['cancellation_cutoff_hours'] = $this->sanitize_integer( $input['cancellation_cutoff_hours'], 0, 8760 ); } if ( array_key_exists( 'rescheduling_cutoff_hours', $input ) ) { $settings['rescheduling_cutoff_hours'] = $this->sanitize_integer( $input['rescheduling_cutoff_hours'], 0, 8760 ); } if ( array_key_exists( 'admin_notification_email', $input ) ) { $settings['admin_notification_email'] = sanitize_email( $input['admin_notification_email'] ); } if ( array_key_exists( 'email_from_name', $input ) ) { $settings['email_from_name'] = sanitize_text_field( $input['email_from_name'] ); } if ( array_key_exists( 'email_from_address', $input ) ) { $settings['email_from_address'] = sanitize_email( $input['email_from_address'] ); } if ( array_key_exists( 'email_reply_to_address', $input ) ) { $settings['email_reply_to_address'] = sanitize_email( $input['email_reply_to_address'] ); } if ( $is_full_form || 'communication' === $active_tab ) { $settings['email_show_credit'] = ! empty( $input['email_show_credit'] ); foreach ( array_keys( EmailService::template_labels() ) as $template_key ) { $settings[ 'email_' . $template_key . '_enabled' ] = ! empty( $input[ 'email_' . $template_key . '_enabled' ] ); if ( array_key_exists( 'email_' . $template_key . '_subject', $input ) ) { $settings[ 'email_' . $template_key . '_subject' ] = sanitize_text_field( $input[ 'email_' . $template_key . '_subject' ] ); } if ( array_key_exists( 'email_' . $template_key . '_body', $input ) ) { $settings[ 'email_' . $template_key . '_body' ] = wp_kses_post( $input[ 'email_' . $template_key . '_body' ] ); } } } if ( array_key_exists( 'form_primary_color', $input ) ) { $settings['form_primary_color'] = sanitize_hex_color( $input['form_primary_color'] ) ?: $previous['form_primary_color']; } if ( array_key_exists( 'form_background_color', $input ) ) { $settings['form_background_color'] = sanitize_hex_color( $input['form_background_color'] ) ?: $previous['form_background_color']; } if ( array_key_exists( 'form_background_opacity', $input ) ) { $settings['form_background_opacity'] = $this->sanitize_integer( $input['form_background_opacity'], 0, 100 ); } if ( array_key_exists( 'form_text_color', $input ) ) { $settings['form_text_color'] = sanitize_hex_color( $input['form_text_color'] ) ?: $previous['form_text_color']; } if ( array_key_exists( 'form_border_color', $input ) ) { $settings['form_border_color'] = sanitize_hex_color( $input['form_border_color'] ) ?: $previous['form_border_color']; } if ( array_key_exists( 'form_service_border_color', $input ) ) { $settings['form_service_border_color'] = sanitize_hex_color( $input['form_service_border_color'] ) ?: $previous['form_service_border_color']; } if ( array_key_exists( 'form_service_background_color', $input ) ) { $settings['form_service_background_color'] = sanitize_hex_color( $input['form_service_background_color'] ) ?: $previous['form_service_background_color']; } if ( array_key_exists( 'form_category_background_color', $input ) ) { $settings['form_category_background_color'] = sanitize_hex_color( $input['form_category_background_color'] ) ?: $previous['form_category_background_color']; } if ( array_key_exists( 'form_field_text_color', $input ) ) { $settings['form_field_text_color'] = sanitize_hex_color( $input['form_field_text_color'] ) ?: $previous['form_field_text_color']; } if ( array_key_exists( 'form_field_background_color', $input ) ) { $settings['form_field_background_color'] = sanitize_hex_color( $input['form_field_background_color'] ) ?: $previous['form_field_background_color']; } if ( array_key_exists( 'form_field_border_color', $input ) ) { $settings['form_field_border_color'] = sanitize_hex_color( $input['form_field_border_color'] ) ?: $previous['form_field_border_color']; } if ( array_key_exists( 'form_font_family', $input ) ) { $settings['form_font_family'] = $this->sanitize_choice( (string) $input['form_font_family'], array( 'system', 'serif', 'mono', 'rounded' ), 'system' ); } if ( array_key_exists( 'form_density', $input ) ) { $settings['form_density'] = $this->sanitize_choice( (string) $input['form_density'], array( 'compact', 'comfortable' ), 'compact' ); } if ( array_key_exists( 'form_shadow', $input ) ) { $settings['form_shadow'] = $this->sanitize_choice( (string) $input['form_shadow'], array( 'none', 'soft', 'lifted' ), 'soft' ); } if ( array_key_exists( 'form_look', $input ) ) { $look = sanitize_key( (string) $input['form_look'] ); $settings['form_look'] = array_key_exists( $look, FormLooks::all() ) ? $look : ''; } foreach ( array( 'form_max_width' => array( 360, 1400 ), 'form_padding' => array( 8, 64 ), 'form_font_size' => array( 13, 19 ), 'form_container_border_radius' => array( 0, 32 ), 'form_border_radius' => array( 0, 32 ), 'form_hours_border_radius' => array( 0, 32 ), 'form_border_width' => array( 0, 8 ), 'form_service_border_width' => array( 0, 8 ), 'form_field_border_radius' => array( 0, 32 ), 'form_field_border_width' => array( 0, 8 ), 'form_image_size' => array( 32, 240 ), 'form_image_radius' => array( 0, 48 ), 'form_image_border_width' => array( 0, 8 ), 'form_map_height' => array( 120, 520 ), ) as $key => $bounds ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = $this->sanitize_integer( $input[ $key ], $bounds[0], $bounds[1] ); } } if ( array_key_exists( 'form_service_layout', $input ) ) { $settings['form_service_layout'] = $this->sanitize_choice( (string) $input['form_service_layout'], array( 'list', 'grid', 'compact' ), 'list' ); } if ( array_key_exists( 'form_service_border_color_mode', $input ) ) { $settings['form_service_border_color_mode'] = $this->sanitize_choice( (string) $input['form_service_border_color_mode'], array( 'service', 'custom' ), 'custom' ); } if ( array_key_exists( 'form_service_background_color_mode', $input ) ) { $settings['form_service_background_color_mode'] = $this->sanitize_choice( (string) $input['form_service_background_color_mode'], array( 'service', 'custom' ), 'custom' ); } if ( array_key_exists( 'form_category_behavior', $input ) ) { $settings['form_category_behavior'] = $this->sanitize_choice( (string) $input['form_category_behavior'], array( 'expanded', 'collapsed' ), 'collapsed' ); } if ( array_key_exists( 'form_image_fit', $input ) ) { $settings['form_image_fit'] = $this->sanitize_choice( (string) $input['form_image_fit'], array( 'cover', 'contain' ), 'cover' ); } if ( $is_full_form || 'form-frontend' === $active_tab ) { $settings['form_group_services'] = ! empty( $input['form_group_services'] ); $settings['form_show_service_images'] = ! empty( $input['form_show_service_images'] ); $settings['form_show_category_images'] = ! empty( $input['form_show_category_images'] ); $settings['form_show_map'] = ! empty( $input['form_show_map'] ); $settings['form_auto_text_color'] = ! empty( $input['form_auto_text_color'] ); } if ( array_key_exists( 'form_title', $input ) ) { $settings['form_title'] = sanitize_text_field( $input['form_title'] ); } if ( array_key_exists( 'form_intro', $input ) ) { $settings['form_intro'] = sanitize_textarea_field( $input['form_intro'] ); } if ( array_key_exists( 'form_button_label', $input ) ) { $settings['form_button_label'] = sanitize_text_field( $input['form_button_label'] ); } if ( array_key_exists( 'form_display_mode', $input ) ) { $settings['form_display_mode'] = $this->sanitize_choice( $input['form_display_mode'], array( 'embedded', 'popup' ), 'embedded' ); } if ( array_key_exists( 'confirmation_message', $input ) ) { $settings['confirmation_message'] = sanitize_textarea_field( $input['confirmation_message'] ); } if ( array_key_exists( 'booking_terms', $input ) ) { $settings['booking_terms'] = wp_kses_post( is_string( $input['booking_terms'] ) ? $input['booking_terms'] : '' ); } if ( array_key_exists( 'form_fields', $input ) ) { $settings['form_fields'] = self::normalize_form_fields( $input['form_fields'] ); } if ( 'form-frontend' === $active_tab || array_key_exists( 'require_policy_checkbox', $input ) ) { $settings['require_policy_checkbox'] = ! empty( $input['require_policy_checkbox'] ); } if ( array_key_exists( 'policy_checkbox_text', $input ) ) { $settings['policy_checkbox_text'] = sanitize_text_field( $input['policy_checkbox_text'] ); } if ( array_key_exists( 'default_payment_status', $input ) ) { $settings['default_payment_status'] = $this->sanitize_choice( $input['default_payment_status'], array( 'not_required', 'unpaid' ), 'unpaid' ); } if ( array_key_exists( 'payment_instructions', $input ) ) { $settings['payment_instructions'] = sanitize_textarea_field( $input['payment_instructions'] ); } if ( array_key_exists( 'pay_on_site_enabled', $input ) ) { $settings['pay_on_site_enabled'] = ! empty( $input['pay_on_site_enabled'] ); } if ( $is_full_form || 'privacy-data' === $active_tab ) { $settings['delete_data_on_uninstall'] = ! empty( $input['delete_data_on_uninstall'] ); } if ( $is_full_form || 'advanced' === $active_tab ) { $settings['debug_mode'] = ! empty( $input['debug_mode'] ); } if ( empty( $settings['admin_notification_email'] ) ) { $settings['admin_notification_email'] = get_option( 'admin_email' ); } if ( empty( $settings['email_from_address'] ) ) { $settings['email_from_address'] = get_option( 'admin_email' ); } if ( empty( $settings['email_reply_to_address'] ) ) { $settings['email_reply_to_address'] = $settings['email_from_address']; } return $settings; } /** * Sanitizes a complete settings import without relying on the active settings tab. * * @param array $input Imported settings. * @return array */ public function sanitize_import( array $input ): array { BookingLock::require_configuration_lock(); $settings = self::values(); if ( array_key_exists( 'business_name', $input ) ) { $settings['business_name'] = sanitize_text_field( $input['business_name'] ); } $settings = self::sanitize_header_name( $input, $settings ); if ( array_key_exists( 'business_timezone', $input ) ) { $settings['business_timezone'] = $this->sanitize_timezone( (string) $input['business_timezone'] ); } if ( array_key_exists( 'date_display_format', $input ) ) { $settings['date_display_format'] = $this->sanitize_choice( (string) $input['date_display_format'], array_keys( DateTimeHelper::date_format_choices() ), 'wordpress' ); } if ( array_key_exists( 'time_display_format', $input ) ) { $settings['time_display_format'] = $this->sanitize_choice( (string) $input['time_display_format'], array_keys( DateTimeHelper::time_format_choices() ), 'wordpress' ); } if ( array_key_exists( 'currency', $input ) ) { $settings['currency'] = $this->sanitize_currency( (string) $input['currency'] ); if ( $settings['currency'] !== self::values()['currency'] && ! $this->currency_prices_compatible( $settings['currency'] ) ) { throw new \InvalidArgumentException( esc_html__( 'Some saved service or option prices contain fractions. Adjust those prices before choosing a whole-unit currency.', 'abientot' ) ); } } foreach ( array( 'booking_min_notice_hours' => array( 0, 8760 ), 'booking_window_days' => array( 1, 730 ), 'booking_slot_interval_minutes' => array( 5, 240 ), 'cancellation_cutoff_hours' => array( 0, 8760 ), 'rescheduling_cutoff_hours' => array( 0, 8760 ), ) as $key => $bounds ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = $this->sanitize_integer( $input[ $key ], $bounds[0], $bounds[1] ); } } foreach ( array( 'match_customer_by_email', 'pay_on_site_enabled', 'match_customer_by_phone', 'email_customer_confirmation_enabled', 'email_admin_new_booking_enabled', 'email_customer_cancellation_enabled', 'email_customer_rescheduling_enabled', 'email_customer_completed_enabled', 'email_show_credit', 'require_policy_checkbox', 'form_group_services', 'form_show_service_images', 'form_show_category_images', 'form_show_map', 'form_auto_text_color', 'delete_data_on_uninstall', 'debug_mode', ) as $key ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = ! empty( $input[ $key ] ); } } if ( array_key_exists( 'default_booking_status', $input ) ) { $settings['default_booking_status'] = $this->sanitize_choice( (string) $input['default_booking_status'], array( 'pending', 'confirmed' ), 'pending' ); } if ( array_key_exists( 'form_display_mode', $input ) ) { $settings['form_display_mode'] = $this->sanitize_choice( (string) $input['form_display_mode'], array( 'embedded', 'popup' ), 'embedded' ); } if ( array_key_exists( 'form_service_layout', $input ) ) { $settings['form_service_layout'] = $this->sanitize_choice( (string) $input['form_service_layout'], array( 'list', 'grid', 'compact' ), 'list' ); } if ( array_key_exists( 'form_service_border_color_mode', $input ) ) { $settings['form_service_border_color_mode'] = $this->sanitize_choice( (string) $input['form_service_border_color_mode'], array( 'service', 'custom' ), 'custom' ); } if ( array_key_exists( 'form_service_background_color_mode', $input ) ) { $settings['form_service_background_color_mode'] = $this->sanitize_choice( (string) $input['form_service_background_color_mode'], array( 'service', 'custom' ), 'custom' ); } if ( array_key_exists( 'form_category_behavior', $input ) ) { $settings['form_category_behavior'] = $this->sanitize_choice( (string) $input['form_category_behavior'], array( 'expanded', 'collapsed' ), 'collapsed' ); } if ( array_key_exists( 'form_image_fit', $input ) ) { $settings['form_image_fit'] = $this->sanitize_choice( (string) $input['form_image_fit'], array( 'cover', 'contain' ), 'cover' ); } if ( array_key_exists( 'form_border_color', $input ) ) { $settings['form_border_color'] = sanitize_hex_color( (string) $input['form_border_color'] ) ?: $settings['form_border_color']; } if ( array_key_exists( 'form_service_border_color', $input ) ) { $settings['form_service_border_color'] = sanitize_hex_color( (string) $input['form_service_border_color'] ) ?: $settings['form_service_border_color']; } if ( array_key_exists( 'form_service_background_color', $input ) ) { $settings['form_service_background_color'] = sanitize_hex_color( (string) $input['form_service_background_color'] ) ?: $settings['form_service_background_color']; } if ( array_key_exists( 'form_category_background_color', $input ) ) { $settings['form_category_background_color'] = sanitize_hex_color( (string) $input['form_category_background_color'] ) ?: $settings['form_category_background_color']; } if ( array_key_exists( 'form_field_text_color', $input ) ) { $settings['form_field_text_color'] = sanitize_hex_color( (string) $input['form_field_text_color'] ) ?: $settings['form_field_text_color']; } if ( array_key_exists( 'form_field_background_color', $input ) ) { $settings['form_field_background_color'] = sanitize_hex_color( (string) $input['form_field_background_color'] ) ?: $settings['form_field_background_color']; } if ( array_key_exists( 'form_field_border_color', $input ) ) { $settings['form_field_border_color'] = sanitize_hex_color( (string) $input['form_field_border_color'] ) ?: $settings['form_field_border_color']; } foreach ( array( 'form_max_width' => array( 360, 1400 ), 'form_padding' => array( 8, 64 ), 'form_font_size' => array( 13, 19 ), 'form_container_border_radius' => array( 0, 32 ), 'form_border_radius' => array( 0, 32 ), 'form_hours_border_radius' => array( 0, 32 ), 'form_border_width' => array( 0, 8 ), 'form_service_border_width' => array( 0, 8 ), 'form_field_border_radius' => array( 0, 32 ), 'form_field_border_width' => array( 0, 8 ), 'form_image_size' => array( 32, 240 ), 'form_image_radius' => array( 0, 48 ), 'form_image_border_width' => array( 0, 8 ), 'form_map_height' => array( 120, 520 ), ) as $key => $bounds ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = $this->sanitize_integer( $input[ $key ], $bounds[0], $bounds[1] ); } } if ( array_key_exists( 'default_payment_status', $input ) ) { $settings['default_payment_status'] = $this->sanitize_choice( (string) $input['default_payment_status'], array( 'not_required', 'unpaid' ), 'unpaid' ); } foreach ( array( 'admin_notification_email', 'email_from_address', 'email_reply_to_address' ) as $key ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = sanitize_email( $input[ $key ] ); } } foreach ( array( 'email_from_name', 'form_title', 'form_button_label', 'policy_checkbox_text' ) as $key ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = sanitize_text_field( $input[ $key ] ); } } foreach ( array( 'form_intro', 'confirmation_message', 'payment_instructions' ) as $key ) { if ( array_key_exists( $key, $input ) ) { $settings[ $key ] = sanitize_textarea_field( $input[ $key ] ); } } if ( array_key_exists( 'booking_terms', $input ) ) { $settings['booking_terms'] = wp_kses_post( is_string( $input['booking_terms'] ) ? $input['booking_terms'] : '' ); } if ( array_key_exists( 'form_fields', $input ) ) { $settings['form_fields'] = self::normalize_form_fields( $input['form_fields'] ); } if ( array_key_exists( 'form_primary_color', $input ) ) { $settings['form_primary_color'] = sanitize_hex_color( $input['form_primary_color'] ) ?: self::defaults()['form_primary_color']; } if ( array_key_exists( 'form_background_color', $input ) ) { $settings['form_background_color'] = sanitize_hex_color( $input['form_background_color'] ) ?: self::defaults()['form_background_color']; } if ( array_key_exists( 'form_background_opacity', $input ) ) { $settings['form_background_opacity'] = $this->sanitize_integer( $input['form_background_opacity'], 0, 100 ); } if ( array_key_exists( 'form_text_color', $input ) ) { $settings['form_text_color'] = sanitize_hex_color( $input['form_text_color'] ) ?: self::defaults()['form_text_color']; } if ( array_key_exists( 'form_font_family', $input ) ) { $settings['form_font_family'] = $this->sanitize_choice( (string) $input['form_font_family'], array( 'system', 'serif', 'mono', 'rounded' ), 'system' ); } if ( array_key_exists( 'form_density', $input ) ) { $settings['form_density'] = $this->sanitize_choice( (string) $input['form_density'], array( 'compact', 'comfortable' ), 'compact' ); } if ( array_key_exists( 'form_shadow', $input ) ) { $settings['form_shadow'] = $this->sanitize_choice( (string) $input['form_shadow'], array( 'none', 'soft', 'lifted' ), 'soft' ); } if ( array_key_exists( 'form_look', $input ) ) { $look = sanitize_key( (string) $input['form_look'] ); $settings['form_look'] = array_key_exists( $look, FormLooks::all() ) ? $look : ''; } foreach ( array_keys( EmailService::template_labels() ) as $template_key ) { $enabled_key = 'email_' . $template_key . '_enabled'; $subject_key = 'email_' . $template_key . '_subject'; $body_key = 'email_' . $template_key . '_body'; if ( array_key_exists( $enabled_key, $input ) ) { $settings[ $enabled_key ] = ! empty( $input[ $enabled_key ] ); } if ( array_key_exists( $subject_key, $input ) ) { $settings[ $subject_key ] = sanitize_text_field( $input[ $subject_key ] ); } if ( array_key_exists( $body_key, $input ) ) { $settings[ $body_key ] = wp_kses_post( $input[ $body_key ] ); } } if ( empty( $settings['admin_notification_email'] ) ) { $settings['admin_notification_email'] = get_option( 'admin_email' ); } if ( empty( $settings['email_from_address'] ) ) { $settings['email_from_address'] = get_option( 'admin_email' ); } if ( empty( $settings['email_reply_to_address'] ) ) { $settings['email_reply_to_address'] = $settings['email_from_address']; } return $settings; } /** * @return array */ private static function email_template_defaults(): array { $defaults = array(); foreach ( EmailService::template_subject_defaults() as $template_key => $subject ) { $defaults[ 'email_' . $template_key . '_subject' ] = $subject; } foreach ( EmailService::template_body_defaults() as $template_key => $body ) { $defaults[ 'email_' . $template_key . '_body' ] = $body; } return $defaults; } /** * Languages bundled with the plugin. An empty value follows WordPress. * * @return array */ public static function language_names(): array { // This registry is also read during gettext loading: native names must not // call gettext, or resolving the configured language would recurse. return array( 'en_US' => 'English', 'fr_FR' => 'Français', 'pt_PT' => 'Português', 'de_DE' => 'Deutsch', 'es_ES' => 'Español', 'it_IT' => 'Italiano', 'pt_BR' => 'Português do Brasil', 'ja' => '日本語', 'nl_NL' => 'Nederlands', 'pl_PL' => 'Polski', 'tr_TR' => 'Türkçe', 'zh_CN' => '简体中文', 'ko_KR' => '한국어', 'id_ID' => 'Bahasa Indonesia', 'ar' => 'العربية', 'sv_SE' => 'Svenska', 'cs_CZ' => 'Čeština', 'da_DK' => 'Dansk', 'nb_NO' => 'Norsk bokmål', 'vi' => 'Tiếng Việt', 'fi' => 'Suomi', 'ro_RO' => 'Română', ); } public static function language_choices(): array { return array( '' => __( 'WordPress language (default)', 'abientot' ) ) + self::language_names(); } public static function configured_locale(): string { $settings = get_option( self::OPTION_NAME, array() ); $locale = is_array( $settings ) && isset( $settings['plugin_language'] ) ? sanitize_text_field( (string) $settings['plugin_language'] ) : ''; return isset( self::language_names()[ $locale ] ) ? $locale : ''; } public static function template_language_label(): string { $locale = self::configured_locale(); $locale = '' !== $locale ? $locale : get_locale(); $names = self::language_names(); if ( isset( $names[ $locale ] ) ) { return $names[ $locale ]; } foreach ( $names as $code => $name ) { if ( strtok( $locale, '_-' ) === strtok( $code, '_-' ) ) { return $name; } } return 'English'; } private function sanitize_timezone( string $timezone ): string { $timezone = sanitize_text_field( $timezone ); return in_array( $timezone, timezone_identifiers_list(), true ) ? $timezone : wp_timezone_string(); } private function sanitize_language( string $locale ): string { $locales = array(); foreach ( array_keys( self::language_names() ) as $code ) { $locales[ strtolower( $code ) ] = $code; } $key = strtolower( sanitize_text_field( $locale ) ); return $locales[ $key ] ?? ''; } private function sanitize_currency( string $currency ): string { $currency = strtoupper( sanitize_text_field( $currency ) ); return in_array( $currency, self::currency_codes(), true ) ? $currency : 'USD'; } /** * Returns the 179 selectable ISO 4217 currency and fund codes. * * The testing code (XTS) and no-currency code (XXX) are intentionally excluded. * * @return string[] */ public static function currency_codes(): array { return explode( ',', 'AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BOV,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHE,CHF,CHW,CLF,CLP,CNY,COP,COU,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MXV,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,USN,UYI,UYU,UYW,UZS,VED,VES,VND,VUV,WST,XAF,XAG,XAU,XBA,XBB,XBC,XBD,XCD,XDR,XOF,XPD,XPF,XPT,XSU,XUA,YER,ZAR,ZMW,ZWL' ); } /** * @param array $labels Posted status display labels keyed by status ID. */ private function sanitize_status_labels( array $labels ): void { $statuses = new StatusesRepository(); foreach ( $labels as $id => $label ) { $id = absint( $id ); $label = sanitize_text_field( $label ); if ( $id > 0 && '' !== $label && $statuses->find( $id ) ) { $statuses->update( $id, array( 'label' => $label, 'updated_at' => gmdate( 'Y-m-d H:i:s' ), ) ); } } } private function sanitize_integer( $value, int $minimum, int $maximum ): int { return max( $minimum, min( $maximum, absint( $value ) ) ); } /** * @param string $value Candidate value. * @param string[] $choices Allowed choices. * @param string $default Default fallback. */ private function sanitize_choice( string $value, array $choices, string $default ): string { $value = sanitize_key( $value ); return in_array( $value, $choices, true ) ? $value : $default; } private function currency_prices_compatible( string $currency ): bool { if ( \Abientot\Support\Currency::precision( $currency ) !== 0 ) { return true; } global $wpdb; $prefix = $wpdb->prefix . 'abientot_'; $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE MOD(price_cents,100) <> 0', $prefix . 'services' ) ); if ( $count === null || (int) $count > 0 ) { return false; } $table = $prefix . 'service_selection_definitions'; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) !== $table ) { return true; } $definitions = $wpdb->get_col( $wpdb->prepare( 'SELECT definition_json FROM %i', $table ) ); if ( $wpdb->last_error ) { return false; } foreach ( $definitions as$json ) { $definition = json_decode( $json, true ); if ( ! is_array( $definition ) ) { return false; } foreach ( array_merge( is_array( $definition['variants'] ?? null ) ? $definition['variants'] : array(), is_array( $definition['extras'] ?? null ) ? $definition['extras'] : array() )as$line ) { if ( (int) ( $line['price_delta_cents'] ?? 0 ) % 100 !== 0 ) { return false; } } } return true; } private function sync_service_currency( string $previous_currency, string $current_currency ): void { $previous_currency = $this->sanitize_currency( $previous_currency ); $current_currency = $this->sanitize_currency( $current_currency ); if ( $previous_currency === $current_currency ) { return; } ( new ServicesRepository() )->update_all_currency( $current_currency ); } private function settings_redirect_url( string $return_page ): string { if ( 'abientot-form' === $return_page ) { return add_query_arg( array( 'page' => 'abientot-form', 'settings-updated' => 'true', ), admin_url( 'admin.php' ) ); } $redirect = wp_get_referer(); if ( ! $redirect || false !== strpos( $redirect, '/options.php' ) || false !== strpos( $redirect, '/admin-post.php' ) ) { $redirect = admin_url( 'admin.php?page=abientot-settings' ); } $redirect = remove_query_arg( array( 'settings-updated', '_wpnonce' ), $redirect ); return add_query_arg( 'settings-updated', 'true', $redirect ); } }