'array', 'sanitize_callback' => array( __CLASS__, 'sanitize' ), 'default' => AIDF_Plugin::get_defaults(), ) ); } /** * Sanitise all settings on save. * * Note: $input is already unslashed by options.php before reaching this * callback, so wp_unslash() must NOT be called again here. * * @param array $input Raw form input (already unslashed). * @return array */ public static function sanitize( $input ) { $clean = array(); $defaults = AIDF_Plugin::get_defaults(); $saved = get_option( 'aidf_settings', array() ); if ( ! is_array( $saved ) ) { $saved = array(); } // Merge saved settings with defaults so missing fields preserve their saved value. $fallback = wp_parse_args( $saved, $defaults ); // Identity — text fields. $text_fields = array( 'business_name', 'legal_name', 'tagline', 'contact_email', 'contact_phone', 'address', 'postcode', 'location_city', 'location_region', 'location_country', 'company_number', 'company_jurisdiction', 'vat_number', 'operating_hours', 'brand_pronunciation', 'citation_format', ); foreach ( $text_fields as $field ) { $clean[ $field ] = isset( $input[ $field ] ) ? sanitize_text_field( $input[ $field ] ) : $fallback[ $field ]; } // Identity — URL fields. $url_fields = array( 'contact_url', 'logo_url', 'products_url', 'privacy_policy_url', 'terms_url' ); foreach ( $url_fields as $url_field ) { $clean[ $url_field ] = isset( $input[ $url_field ] ) ? esc_url_raw( $input[ $url_field ] ) : $fallback[ $url_field ]; } // Identity — email validation. if ( ! empty( $clean['contact_email'] ) && ! is_email( $clean['contact_email'] ) ) { $clean['contact_email'] = $fallback['contact_email']; add_settings_error( 'aidf_settings', 'invalid_email', __( 'Invalid contact email address. Previous value retained.', 'ai-discovery-files' ) ); } // Permissions — select fields. $clean['ai_usage'] = self::sanitize_select( $input, 'ai_usage', array( 'allow', 'allow_with_attribution', 'restrict', 'prohibit' ), $fallback['ai_usage'] ); $clean['ai_training'] = self::sanitize_select( $input, 'ai_training', array( 'allow', 'restrict', 'prohibit' ), $fallback['ai_training'] ); $clean['crawler_policy'] = self::sanitize_select( $input, 'crawler_policy', array( 'allow_all', 'allow_known', 'restrict' ), $fallback['crawler_policy'] ); $clean['content_licence'] = self::sanitize_select( $input, 'content_licence', array( '', 'all_rights_reserved', 'cc_by', 'cc_by_sa', 'cc_by_nc', 'cc_by_nd', 'cc0' ), $fallback['content_licence'] ); // Identity — select fields with defined option lists. $clean['industry'] = self::sanitize_select( $input, 'industry', array_merge( array( '' ), array_keys( AIDF_Plugin::get_industries() ) ), $fallback['industry'] ); $clean['business_type'] = self::sanitize_select( $input, 'business_type', array( '', 'Sole Trader', 'Partnership', 'Limited Company', 'Limited Liability Partnership', 'Public Limited Company', 'Corporation', 'LLC', 'Non-Profit', 'Government', 'Cooperative', 'Freelancer', 'Other', ), $fallback['business_type'] ); $clean['employee_count'] = self::sanitize_select( $input, 'employee_count', array( '', 'Solo', '2-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5001-10000', '10001+' ), $fallback['employee_count'] ); // Identity — founded year (4-digit year or empty). if ( isset( $input['founded_year'] ) ) { $year = sanitize_text_field( $input['founded_year'] ); if ( '' === $year ) { $clean['founded_year'] = ''; } elseif ( preg_match( '/^\d{4}$/', $year ) && (int) $year >= 1800 && (int) $year <= (int) gmdate( 'Y' ) ) { $clean['founded_year'] = $year; } else { $clean['founded_year'] = $fallback['founded_year']; add_settings_error( 'aidf_settings', 'invalid_year', __( 'Invalid founded year. Please enter a 4-digit year (e.g. 2010). Previous value retained.', 'ai-discovery-files' ) ); } } else { $clean['founded_year'] = $fallback['founded_year']; } $clean['customer_type'] = self::sanitize_select( $input, 'customer_type', array( '', 'B2B', 'B2C', 'Both' ), $fallback['customer_type'] ); $clean['offering_type'] = self::sanitize_select( $input, 'offering_type', array( '', 'Products', 'Services', 'Both' ), $fallback['offering_type'] ); $clean['operating_hours_preset'] = self::sanitize_select( $input, 'operating_hours_preset', array_keys( AIDF_Plugin::get_operating_hours_presets() ), $fallback['operating_hours_preset'] ); // Identity — boolean fields (preserve if not submitted from this tab). if ( isset( $input['vat_registered'] ) ) { $clean['vat_registered'] = ! empty( $input['vat_registered'] ); } else { $clean['vat_registered'] = ! empty( $fallback['vat_registered'] ); } if ( isset( $input['is_ecommerce'] ) ) { $clean['is_ecommerce'] = ! empty( $input['is_ecommerce'] ); } else { $clean['is_ecommerce'] = ! empty( $fallback['is_ecommerce'] ); } // Content — textarea fields. $textarea_fields = array( 'not_services', 'service_areas', 'not_service_areas', 'social_links', 'brand_alternates', 'brand_never', 'brand_misspellings', 'brand_voice', 'brand_taglines', 'brand_boilerplate', 'developer_notes', 'api_info', 'tech_stack', ); foreach ( $textarea_fields as $field ) { $clean[ $field ] = isset( $input[ $field ] ) ? sanitize_textarea_field( $input[ $field ] ) : $fallback[ $field ]; } // Services — structured repeater array (preserve existing if not submitted). if ( isset( $input['services'] ) ) { $clean['services'] = self::sanitize_services( $input ); } else { $clean['services'] = isset( $fallback['services'] ) ? $fallback['services'] : array(); } // Key People — structured repeater array (preserve existing if not submitted). if ( isset( $input['key_people'] ) ) { $clean['key_people'] = self::sanitize_key_people( $input ); } else { $clean['key_people'] = isset( $fallback['key_people'] ) ? $fallback['key_people'] : array(); } // FAQs — structured array (preserve existing if not submitted). if ( isset( $input['faqs'] ) ) { $clean['faqs'] = self::sanitize_faqs( $input ); } else { $clean['faqs'] = isset( $fallback['faqs'] ) ? $fallback['faqs'] : array(); } // Active files — checkbox array. $clean['active_files'] = self::sanitize_active_files( $input ); // Spec attribution — boolean (preserve if not submitted). if ( isset( $input['spec_attribution'] ) ) { $clean['spec_attribution'] = ! empty( $input['spec_attribution'] ); } else { $clean['spec_attribution'] = ! empty( $fallback['spec_attribution'] ); } // Directory verification code — hex only (prefix stripped on save). if ( isset( $input['verify_code'] ) ) { $code = sanitize_text_field( $input['verify_code'] ); $code = preg_replace( '/^ai-visibility-verify=/i', '', $code ); $clean['verify_code'] = ( '' === $code || preg_match( '/^[a-f0-9]+$/i', $code ) ) ? $code : $fallback['verify_code']; } else { $clean['verify_code'] = isset( $fallback['verify_code'] ) ? $fallback['verify_code'] : ''; } // Preserve crawler analytics settings. // Read from $input first (AJAX save passes crawler keys via update_option), // fall back to $fallback (non-crawler tab saves don't include these keys). $clean['crawler_logging_enabled'] = isset( $input['crawler_logging_enabled'] ) ? (bool) $input['crawler_logging_enabled'] : ( isset( $fallback['crawler_logging_enabled'] ) ? (bool) $fallback['crawler_logging_enabled'] : false ); $clean['crawler_log_retention'] = isset( $input['crawler_log_retention'] ) ? sanitize_text_field( $input['crawler_log_retention'] ) : ( isset( $fallback['crawler_log_retention'] ) ? $fallback['crawler_log_retention'] : '90' ); $clean['crawler_enabled_bots'] = isset( $input['crawler_enabled_bots'] ) && is_array( $input['crawler_enabled_bots'] ) ? array_map( 'sanitize_text_field', $input['crawler_enabled_bots'] ) : ( isset( $fallback['crawler_enabled_bots'] ) ? $fallback['crawler_enabled_bots'] : array() ); // Flush rewrite rules since active files may have changed. AIDF_Server::register_rewrite_rules(); flush_rewrite_rules(); // Record the first time settings were saved (for review prompt trigger). if ( false === get_option( 'aidf_settings_saved_at' ) ) { update_option( 'aidf_settings_saved_at', time(), false ); } return $clean; } /** * Sanitise a select field value against allowed options. * * @param array $input Form input. * @param string $key Field key. * @param array $allowed Allowed values. * @param string $default Default value. * @return string */ private static function sanitize_select( $input, $key, $allowed, $default ) { $value = isset( $input[ $key ] ) ? sanitize_text_field( $input[ $key ] ) : $default; return in_array( $value, $allowed, true ) ? $value : $default; } /** * Sanitise FAQ entries. * * @param array $input Form input. * @return array> */ private static function sanitize_faqs( $input ) { $faqs = array(); if ( ! isset( $input['faqs'] ) || ! is_array( $input['faqs'] ) ) { return $faqs; } foreach ( $input['faqs'] as $faq ) { if ( ! is_array( $faq ) ) { continue; } $q = isset( $faq['q'] ) ? sanitize_text_field( $faq['q'] ) : ''; $a = isset( $faq['a'] ) ? sanitize_textarea_field( $faq['a'] ) : ''; if ( ! empty( $q ) && ! empty( $a ) ) { $faqs[] = array( 'q' => $q, 'a' => $a, ); } } return $faqs; } /** * Sanitise service entries from repeater fields. * * @param array $input Form input. * @return array> */ private static function sanitize_services( $input ) { $services = array(); if ( ! isset( $input['services'] ) || ! is_array( $input['services'] ) ) { return $services; } foreach ( $input['services'] as $service ) { if ( ! is_array( $service ) ) { continue; } $name = isset( $service['name'] ) ? sanitize_text_field( $service['name'] ) : ''; $url = isset( $service['url'] ) ? esc_url_raw( $service['url'] ) : ''; if ( ! empty( $name ) ) { $entry = array( 'name' => $name ); if ( ! empty( $url ) ) { $entry['url'] = $url; } $services[] = $entry; } } return $services; } /** * Sanitise key people entries from repeater fields. * * @param array $input Form input. * @return array> */ private static function sanitize_key_people( $input ) { $people = array(); if ( ! isset( $input['key_people'] ) || ! is_array( $input['key_people'] ) ) { return $people; } foreach ( $input['key_people'] as $person ) { if ( ! is_array( $person ) ) { continue; } $name = isset( $person['name'] ) ? sanitize_text_field( $person['name'] ) : ''; $role = isset( $person['role'] ) ? sanitize_text_field( $person['role'] ) : ''; $url = isset( $person['url'] ) ? esc_url_raw( $person['url'] ) : ''; if ( ! empty( $name ) ) { $entry = array( 'name' => $name, 'role' => $role, ); if ( ! empty( $url ) ) { $entry['url'] = $url; } $people[] = $entry; } } return $people; } /** * Sanitise the active files array. * * @param array $input Form input. * @return array */ private static function sanitize_active_files( $input ) { $active = array(); $file_types = AIDF_Plugin::get_file_types(); if ( ! isset( $input['active_files'] ) || ! is_array( $input['active_files'] ) ) { return $active; } foreach ( $input['active_files'] as $slug ) { $slug = sanitize_text_field( $slug ); if ( isset( $file_types[ $slug ] ) ) { $active[] = $slug; } } return $active; } }