array( __CLASS__, 'sanitize' ), 'show_in_rest' => false, ) ); add_settings_section( 'fdb_section_keys', __( 'API Keys', 'fontdelbosc-eltemps' ), '__return_false', 'fontdelbosc-eltemps' ); add_settings_section( 'fdb_section_cache', __( 'Cache Settings', 'fontdelbosc-eltemps' ), '__return_false', 'fontdelbosc-eltemps' ); add_settings_section( 'fdb_section_display', __( 'Display Options', 'fontdelbosc-eltemps' ), '__return_false', 'fontdelbosc-eltemps' ); add_settings_field( 'fdb_field_aemet_key', __( 'AEMET API key', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_aemet_key' ), 'fontdelbosc-eltemps', 'fdb_section_keys' ); add_settings_field( 'fdb_field_meteocat_key', __( 'Meteocat API key', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_meteocat_key' ), 'fontdelbosc-eltemps', 'fdb_section_keys' ); add_settings_field( 'fdb_field_ttl_current', __( 'Current conditions cache (seconds)', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_ttl_current' ), 'fontdelbosc-eltemps', 'fdb_section_cache' ); add_settings_field( 'fdb_field_ttl_forecast', __( 'Forecast cache (seconds)', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_ttl_forecast' ), 'fontdelbosc-eltemps', 'fdb_section_cache' ); add_settings_field( 'fdb_field_ttl_historical', __( 'Historical data cache (seconds)', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_ttl_historical' ), 'fontdelbosc-eltemps', 'fdb_section_cache' ); add_settings_field( 'fdb_field_attribution', __( 'Display Options', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_attribution' ), 'fontdelbosc-eltemps', 'fdb_section_display' ); add_settings_field( 'fdb_field_delete', __( 'Uninstall', 'fontdelbosc-eltemps' ), array( __CLASS__, 'field_delete' ), 'fontdelbosc-eltemps', 'fdb_section_display' ); } /** * Sanitizes the option array on save. * * @param mixed $input Raw input. * @return array */ public static function sanitize( mixed $input ): array { $input = is_array( $input ) ? $input : array(); $old = (array) get_option( 'fontdelbosc_options', array() ); $aemet_key = sanitize_text_field( Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $input, 'aemet_api_key' ) ) ?? '' ); $meteo_key = sanitize_text_field( Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $input, 'meteocat_api_key' ) ) ?? '' ); return array( 'aemet_api_key' => '' === $aemet_key ? Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $old, 'aemet_api_key' ) ) ?? '' : $aemet_key, 'meteocat_api_key' => '' === $meteo_key ? Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $old, 'meteocat_api_key' ) ) ?? '' : $meteo_key, 'meteocat_station_code' => sanitize_text_field( Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $old, 'meteocat_station_code' ) ) ?? '' ), 'cache_ttl_current' => max( 60, min( 86400, Fontdelbosc_Payload::int( Fontdelbosc_Payload::get( $input, 'cache_ttl_current' ) ) ?? 600 ) ), 'cache_ttl_forecast' => max( 60, min( 86400, Fontdelbosc_Payload::int( Fontdelbosc_Payload::get( $input, 'cache_ttl_forecast' ) ) ?? 3600 ) ), 'cache_ttl_historical' => max( 60, min( 604800, Fontdelbosc_Payload::int( Fontdelbosc_Payload::get( $input, 'cache_ttl_historical' ) ) ?? 86400 ) ), 'display_attribution' => ! empty( Fontdelbosc_Payload::get( $input, 'display_attribution' ) ), 'delete_data_on_uninstall' => ! empty( Fontdelbosc_Payload::get( $input, 'delete_data_on_uninstall' ) ), ); } /** * Renders the AEMET API key field. The stored key is never rendered; * only its last 4 characters are shown as a hint. * * @return void */ public static function field_aemet_key(): void { $key = Fontdelbosc_Plugin::get_options()['aemet_api_key']; printf( '' ); self::key_hint( $key ); echo '

opendata.aemet.es

'; } /** * Renders the Meteocat API key field. The stored key is never rendered; * only its last 4 characters are shown as a hint. * * @return void */ public static function field_meteocat_key(): void { $key = Fontdelbosc_Plugin::get_options()['meteocat_api_key']; printf( '' ); self::key_hint( $key ); echo '

apidocs.meteocat.gencat.cat

'; } /** * Prints the "current key" hint when a key is stored, leaving the field * itself empty so submitting the form without retyping keeps the key. * * @param string $key Stored API key. * @return void */ private static function key_hint( string $key ): void { if ( '' === $key ) { return; } printf( '

%s

', sprintf( /* translators: %s: last 4 characters of the stored API key. */ esc_html__( 'Current key ends in …%s', 'fontdelbosc-eltemps' ), esc_html( substr( $key, -4 ) ) ) ); } /** * Renders the current conditions TTL field. * * @return void */ public static function field_ttl_current(): void { $value = Fontdelbosc_Plugin::get_options()['cache_ttl_current']; printf( '', esc_attr( (string) $value ) ); } /** * Renders the forecast TTL field. * * @return void */ public static function field_ttl_forecast(): void { $value = Fontdelbosc_Plugin::get_options()['cache_ttl_forecast']; printf( '', esc_attr( (string) $value ) ); } /** * Renders the historical data TTL field. * * @return void */ public static function field_ttl_historical(): void { $value = Fontdelbosc_Plugin::get_options()['cache_ttl_historical']; printf( '', esc_attr( (string) $value ) ); } /** * Renders the attribution toggle. * * @return void */ public static function field_attribution(): void { $options = Fontdelbosc_Plugin::get_options(); printf( '', checked( ! empty( $options['display_attribution'] ), true, false ), esc_html__( 'Show data source attribution', 'fontdelbosc-eltemps' ) ); } /** * Renders the uninstall data-deletion toggle. * * @return void */ public static function field_delete(): void { $options = Fontdelbosc_Plugin::get_options(); printf( '', checked( ! empty( $options['delete_data_on_uninstall'] ), true, false ), esc_html__( 'Delete all plugin data on uninstall', 'fontdelbosc-eltemps' ) ); } /** * Shows an informational notice when no API key is configured and the * plugin is running on the Open-Meteo fallback. * * @return void */ public static function maybe_show_notice(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $options = Fontdelbosc_Plugin::get_options(); if ( '' !== $options['aemet_api_key'] || '' !== $options['meteocat_api_key'] ) { return; } $screen = get_current_screen(); if ( null === $screen || ! in_array( $screen->base, array( 'plugins', 'dashboard', 'options-general' ), true ) ) { return; } printf( '

%s

', esc_html__( 'No weather API keys are configured. Data is served by Open-Meteo (no key required).', 'fontdelbosc-eltemps' ) ); } /** * AJAX handler: tests an API source connectivity. * * @return void */ public static function ajax_test_api(): void { check_ajax_referer( 'fontdelbosc_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'forbidden' ) ); } $raw = isset( $_POST['source'] ) && is_string( $_POST['source'] ) ? wp_unslash( $_POST['source'] ) : ''; $source = sanitize_key( $raw ); $options = Fontdelbosc_Plugin::get_options(); $ok = false; switch ( $source ) { case 'aemet': $ok = '' !== $options['aemet_api_key'] && null !== Fontdelbosc_Api_Aemet::get_daily( $options['aemet_api_key'] ); break; case 'meteocat': $ok = '' !== $options['meteocat_api_key'] && null !== Fontdelbosc_Api_Meteocat::get_hourly( $options['meteocat_api_key'] ); break; case 'open-meteo': $ok = null !== Fontdelbosc_Api_OpenMeteo::get_current(); break; } if ( $ok ) { wp_send_json_success( array( 'message' => 'ok' ) ); } wp_send_json_error( array( 'message' => 'failed' ) ); } /** * AJAX handler: finds and stores the XEMA station closest to Font del Bosc. * * @return void */ public static function ajax_find_station(): void { check_ajax_referer( 'fontdelbosc_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'forbidden' ) ); } $options = Fontdelbosc_Plugin::get_options(); if ( '' === $options['meteocat_api_key'] ) { wp_send_json_error( array( 'message' => 'no-key' ) ); } $station = Fontdelbosc_Api_Meteocat::find_nearest_station( $options['meteocat_api_key'] ); if ( null === $station ) { wp_send_json_error( array( 'message' => 'failed' ) ); } $options['meteocat_station_code'] = $station['codi']; update_option( 'fontdelbosc_options', $options ); wp_send_json_success( $station ); } }