v1.0.0
This commit is contained in:
commit
06153e387d
47 changed files with 5116 additions and 0 deletions
334
admin/class-fontdelbosc-admin.php
Normal file
334
admin/class-fontdelbosc-admin.php
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin settings page (Settings → Font del Bosc Temps) and its AJAX
|
||||
* endpoints for API key tests and nearest-station detection.
|
||||
*
|
||||
* @package FontdelboscEltemps
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* See the file header block for the class documentation.
|
||||
*/
|
||||
final class Fontdelbosc_Admin {
|
||||
|
||||
/**
|
||||
* Registers the admin hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_settings_page' ) );
|
||||
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
|
||||
add_action( 'admin_notices', array( __CLASS__, 'maybe_show_notice' ) );
|
||||
add_action( 'wp_ajax_fontdelbosc_test_api', array( __CLASS__, 'ajax_test_api' ) );
|
||||
add_action( 'wp_ajax_fontdelbosc_find_station', array( __CLASS__, 'ajax_find_station' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the settings page under Settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_settings_page(): void {
|
||||
add_options_page(
|
||||
'Font del Bosc — El Temps',
|
||||
'Font del Bosc Temps',
|
||||
'manage_options',
|
||||
'fontdelbosc-eltemps',
|
||||
array( __CLASS__, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the settings page view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_settings_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require FONTDELBOSC_ELTEMPS_PLUGIN_DIR . 'admin/views/settings-page.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the option group, sections and fields via the Settings API.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_settings(): void {
|
||||
register_setting(
|
||||
'fdb_settings',
|
||||
'fontdelbosc_options',
|
||||
array(
|
||||
'sanitize_callback' => 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<string,mixed>
|
||||
*/
|
||||
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(
|
||||
'<input type="password" autocomplete="new-password" class="regular-text" name="fontdelbosc_options[aemet_api_key]" value="" />'
|
||||
);
|
||||
|
||||
self::key_hint( $key );
|
||||
|
||||
echo '<p class="description"><a href="https://opendata.aemet.es/centrodedescargas/altaUsuario" target="_blank" rel="noopener noreferrer">opendata.aemet.es</a></p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'<input type="password" autocomplete="new-password" class="regular-text" name="fontdelbosc_options[meteocat_api_key]" value="" />'
|
||||
);
|
||||
|
||||
self::key_hint( $key );
|
||||
|
||||
echo '<p class="description"><a href="https://apidocs.meteocat.gencat.cat/" target="_blank" rel="noopener noreferrer">apidocs.meteocat.gencat.cat</a></p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'<p class="description">%s</p>',
|
||||
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(
|
||||
'<input type="number" min="60" max="86400" step="10" class="small-text" name="fontdelbosc_options[cache_ttl_current]" value="%d" />',
|
||||
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(
|
||||
'<input type="number" min="60" max="86400" step="10" class="small-text" name="fontdelbosc_options[cache_ttl_forecast]" value="%d" />',
|
||||
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(
|
||||
'<input type="number" min="60" max="604800" step="10" class="small-text" name="fontdelbosc_options[cache_ttl_historical]" value="%d" />',
|
||||
esc_attr( (string) $value )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the attribution toggle.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function field_attribution(): void {
|
||||
$options = Fontdelbosc_Plugin::get_options();
|
||||
|
||||
printf(
|
||||
'<label><input type="checkbox" name="fontdelbosc_options[display_attribution]" value="1" %s /> %s</label>',
|
||||
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(
|
||||
'<label><input type="checkbox" name="fontdelbosc_options[delete_data_on_uninstall]" value="1" %s /> %s</label>',
|
||||
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(
|
||||
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
|
||||
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 );
|
||||
}
|
||||
}
|
||||
156
admin/views/settings-page.php
Normal file
156
admin/views/settings-page.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
/**
|
||||
* Settings page view: Settings → Font del Bosc Temps.
|
||||
*
|
||||
* @package FontdelboscEltemps
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
$fontdelbosc_options = Fontdelbosc_Plugin::get_options();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
||||
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields( 'fdb_settings' );
|
||||
do_settings_sections( 'fontdelbosc-eltemps' );
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
|
||||
<section>
|
||||
<h2><?php esc_html_e( 'Usage', 'fontdelbosc-eltemps' ); ?></h2>
|
||||
<ul class="fdb-usage">
|
||||
<li><code>[fontdelbosc_eltemps]</code> — <?php esc_html_e( 'Current conditions (default view).', 'fontdelbosc-eltemps' ); ?></li>
|
||||
<li><code>[fontdelbosc_eltemps view="hourly" hours="24"]</code> — <?php esc_html_e( 'Hourly forecast for the following hours. Attribute hours: 1-48 (24 by default).', 'fontdelbosc-eltemps' ); ?></li>
|
||||
<li><code>[fontdelbosc_eltemps view="daily" days="7"]</code> — <?php esc_html_e( 'Daily forecast. Attribute days: 1-7 (7 by default).', 'fontdelbosc-eltemps' ); ?></li>
|
||||
<li><code>[fontdelbosc_eltemps view="historical" year="2024" month="6"]</code> — <?php esc_html_e( 'Historical data for one month. Attributes year and month; by default the last completed month.', 'fontdelbosc-eltemps' ); ?></li>
|
||||
</ul>
|
||||
<p class="description"><?php esc_html_e( 'In the block editor, add a Shortcode block and paste the code. This version ships no dedicated blocks; the shortcode works anywhere shortcodes are supported, including the Shortcode block and text widgets.', 'fontdelbosc-eltemps' ); ?></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><?php esc_html_e( 'Status', 'fontdelbosc-eltemps' ); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">AEMET</th>
|
||||
<td>
|
||||
<button type="button" class="button" data-fdb-test="aemet"><?php esc_html_e( 'Test connection', 'fontdelbosc-eltemps' ); ?></button>
|
||||
<span id="fdb-test-aemet"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Meteocat</th>
|
||||
<td>
|
||||
<button type="button" class="button" data-fdb-test="meteocat"><?php esc_html_e( 'Test connection', 'fontdelbosc-eltemps' ); ?></button>
|
||||
<span id="fdb-test-meteocat"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Open-Meteo</th>
|
||||
<td>
|
||||
<button type="button" class="button" data-fdb-test="open-meteo"><?php esc_html_e( 'Test connection', 'fontdelbosc-eltemps' ); ?></button>
|
||||
<span id="fdb-test-open-meteo"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ( '' !== $fontdelbosc_options['meteocat_api_key'] ) : ?>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Nearest station', 'fontdelbosc-eltemps' ); ?></th>
|
||||
<td>
|
||||
<?php if ( '' !== $fontdelbosc_options['meteocat_station_code'] ) : ?>
|
||||
<?php
|
||||
/* translators: %s: XEMA station code. */
|
||||
printf( esc_html__( 'Nearest station: %s', 'fontdelbosc-eltemps' ), esc_html( $fontdelbosc_options['meteocat_station_code'] ) );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button" id="fdb-find-station"><?php esc_html_e( 'Find nearest station', 'fontdelbosc-eltemps' ); ?></button>
|
||||
<span id="fdb-station-result"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.fontdelboscAdmin =
|
||||
<?php
|
||||
echo wp_json_encode(
|
||||
array(
|
||||
'ajaxurl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'fontdelbosc_admin' ),
|
||||
'ok' => __( 'Connection successful.', 'fontdelbosc-eltemps' ),
|
||||
'fail' => __( 'Connection failed.', 'fontdelbosc-eltemps' ),
|
||||
)
|
||||
);
|
||||
?>
|
||||
;
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
var cfg = window.fontdelboscAdmin || {};
|
||||
|
||||
function post( action, data ) {
|
||||
var body = new URLSearchParams();
|
||||
body.set( 'action', action );
|
||||
body.set( 'nonce', cfg.nonce || '' );
|
||||
Object.keys( data || {} ).forEach( function ( key ) {
|
||||
body.set( key, data[ key ] );
|
||||
} );
|
||||
|
||||
return fetch( cfg.ajaxurl, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: body.toString()
|
||||
} ).then( function ( response ) {
|
||||
return response.json();
|
||||
} );
|
||||
}
|
||||
|
||||
document.querySelectorAll( '[data-fdb-test]' ).forEach( function ( button ) {
|
||||
button.addEventListener( 'click', function () {
|
||||
var source = button.getAttribute( 'data-fdb-test' );
|
||||
var output = document.getElementById( 'fdb-test-' + source );
|
||||
|
||||
if ( output ) {
|
||||
output.textContent = '…';
|
||||
}
|
||||
|
||||
post( 'fontdelbosc_test_api', { source: source } ).then( function ( result ) {
|
||||
if ( output ) {
|
||||
output.textContent = result && result.success ? cfg.ok : cfg.fail;
|
||||
}
|
||||
} ).catch( function () {
|
||||
if ( output ) {
|
||||
output.textContent = cfg.fail;
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
var find = document.getElementById( 'fdb-find-station' );
|
||||
|
||||
if ( find ) {
|
||||
find.addEventListener( 'click', function () {
|
||||
var output = document.getElementById( 'fdb-station-result' );
|
||||
|
||||
if ( output ) {
|
||||
output.textContent = '…';
|
||||
}
|
||||
|
||||
post( 'fontdelbosc_find_station', {} ).then( function ( result ) {
|
||||
if ( output ) {
|
||||
output.textContent = result && result.success && result.data ? result.data.codi : cfg.fail;
|
||||
}
|
||||
} ).catch( function () {
|
||||
if ( output ) {
|
||||
output.textContent = cfg.fail;
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
} )();
|
||||
</script>
|
||||
Loading…
Reference in a new issue