This commit is contained in:
Javier Casares 2026-10-12 15:30:04 +00:00
commit 1ef08bd731
27 changed files with 4854 additions and 0 deletions

View file

@ -0,0 +1,153 @@
<?php
/**
* Admin settings page.
*
* @package Fontdelbosc_Transit
*/
defined( 'ABSPATH' ) || exit;
/**
* Admin UI.
*/
class Fontdelbosc_Transit_Admin {
/**
* Hook the admin pieces.
*/
public static function init(): void {
add_action( 'admin_menu', array( __CLASS__, 'add_menu' ) );
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
add_action( 'admin_post_fontdelbosc_transit_flush', array( __CLASS__, 'handle_flush' ) );
add_action( 'admin_notices', array( __CLASS__, 'notices' ) );
}
/**
* Add the settings page under Settings.
*/
public static function add_menu(): void {
add_options_page(
__( 'Font del Bosc Trànsit', 'fontdelbosc-transit' ),
__( 'Font del Bosc Trànsit', 'fontdelbosc-transit' ),
'manage_options',
'fontdelbosc-transit',
array( __CLASS__, 'render_page' )
);
}
/**
* Register the options entry.
*/
public static function register_settings(): void {
register_setting(
'fontdelbosc_transit',
Fontdelbosc_Transit_Plugin::option_name(),
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize' ),
)
);
}
/**
* Sanitize the submitted options.
*
* @param mixed $input Raw input.
* @return array<string, mixed>
*/
public static function sanitize( $input ): array {
$current = Fontdelbosc_Transit_Plugin::get_options();
$input = is_array( $input ) ? $input : array();
$clean = array(
'incident_cache_ttl' => self::clamp( Fontdelbosc_Transit_Payload::int( $input['incident_cache_ttl'] ?? null ) ?? 300, 60, 86400 ),
'zbe_cache_ttl' => self::clamp( Fontdelbosc_Transit_Payload::int( $input['zbe_cache_ttl'] ?? null ) ?? 86400, 3600, 604800 ),
'default_view' => 'all' === Fontdelbosc_Transit_Payload::str( $input['default_view'] ?? null ) ? 'all' : 'c15',
'max_incidents' => self::clamp( Fontdelbosc_Transit_Payload::int( $input['max_incidents'] ?? null ) ?? 20, 1, 50 ),
'show_zbe_2026_notice' => empty( $input['show_zbe_2026_notice'] ) ? false : true,
'delete_data_on_uninstall' => empty( $input['delete_data_on_uninstall'] ) ? false : true,
);
$zones = array();
if ( isset( $input['zbe_municipalities'] ) && is_array( $input['zbe_municipalities'] ) ) {
foreach ( $input['zbe_municipalities'] as $slug ) {
$zones[] = Fontdelbosc_Transit_Api_Zbe::sanitize_slug( Fontdelbosc_Transit_Payload::str( $slug ) ?? '' );
}
}
$clean['zbe_municipalities'] = array_values( array_unique( array_filter( $zones ) ) );
if ( array() === $clean['zbe_municipalities'] && isset( $input['zbe_municipalities'] ) ) {
$clean['zbe_municipalities'] = (array) $current['zbe_municipalities'];
}
return $clean;
}
/**
* Clamp an integer between bounds.
*
* @param int $value Raw value.
* @param int $min Minimum.
* @param int $max Maximum.
* @return int
*/
private static function clamp( int $value, int $min, int $max ): int {
return max( $min, min( $max, $value ) );
}
/**
* Handle the cache flush action.
*/
public static function handle_flush(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'fontdelbosc-transit' ) );
}
check_admin_referer( 'fontdelbosc_transit_flush' );
Fontdelbosc_Transit_Data_Manager::flush();
wp_safe_redirect(
add_query_arg(
'fontdelbosc_transit_flushed',
'1',
admin_url( 'options-general.php?page=fontdelbosc-transit' )
)
);
exit;
}
/**
* Admin notices: activation 2026 info and flush confirmation.
*/
public static function notices(): void {
if ( get_transient( 'fontdelbosc_transit_2026_notice' ) ) {
delete_transient( 'fontdelbosc_transit_2026_notice' );
echo '<div class="notice notice-info is-dismissible"><p>';
echo '<strong>' . esc_html__( 'Font del Bosc — Trànsit', 'fontdelbosc-transit' ) . ':</strong> ';
echo esc_html__( 'From 1 January 2026, label B vehicles are banned in Catalan ZBEs when high-pollution protocols are activated.', 'fontdelbosc-transit' );
echo '</p></div>';
}
if ( isset( $_GET['fontdelbosc_transit_flushed'] ) && '1' === $_GET['fontdelbosc_transit_flushed'] ) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Cache flushed.', 'fontdelbosc-transit' ) . '</p></div>';
}
}
/**
* Render the settings page.
*/
public static function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'fontdelbosc-transit' ) );
}
$view = FONTDELBOSC_TRANSIT_DIR . 'admin/views/settings-page.php';
if ( file_exists( $view ) ) {
include $view;
}
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* Settings page view.
*
* @package Fontdelbosc_Transit
*/
defined( 'ABSPATH' ) || exit;
/**
* Settings-page variables.
*
* @var array<string, mixed> $fontdelbosc_options
*/
$fontdelbosc_options = Fontdelbosc_Transit_Plugin::get_options();
$fontdelbosc_status = get_option( 'fontdelbosc_transit_status', array() );
$fontdelbosc_index = Fontdelbosc_Transit_Data_Manager::get_zbe_index();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Font del Bosc Trànsit', 'fontdelbosc-transit' ); ?></h1>
<form method="post" action="options.php">
<?php settings_fields( 'fontdelbosc_transit' ); ?>
<h2><?php esc_html_e( 'Incidents', 'fontdelbosc-transit' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="fontdelbosc-transit-incident-ttl"><?php esc_html_e( 'Cache lifetime (seconds)', 'fontdelbosc-transit' ); ?></label></th>
<td>
<input type="number" id="fontdelbosc-transit-incident-ttl" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[incident_cache_ttl]" value="<?php echo esc_attr( (string) $fontdelbosc_options['incident_cache_ttl'] ); ?>" min="60" max="86400" step="60" class="small-text" />
<p class="description"><?php esc_html_e( 'How long incident data is cached. Default 300 (5 minutes); the source updates in near real time.', 'fontdelbosc-transit' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="fontdelbosc-transit-max"><?php esc_html_e( 'Maximum incidents shown', 'fontdelbosc-transit' ); ?></label></th>
<td>
<input type="number" id="fontdelbosc-transit-max" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[max_incidents]" value="<?php echo esc_attr( (string) $fontdelbosc_options['max_incidents'] ); ?>" min="1" max="50" class="small-text" />
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Default view', 'fontdelbosc-transit' ); ?></th>
<td>
<label><input type="radio" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[default_view]" value="c15" <?php checked( $fontdelbosc_options['default_view'], 'c15' ); ?> /> <?php esc_html_e( 'Traffic on C-15', 'fontdelbosc-transit' ); ?></label><br />
<label><input type="radio" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[default_view]" value="all" <?php checked( $fontdelbosc_options['default_view'], 'all' ); ?> /> <?php esc_html_e( 'All Catalunya traffic', 'fontdelbosc-transit' ); ?></label>
<p class="description"><?php esc_html_e( 'What [fontdelbosc_transit] renders when called without a view attribute.', 'fontdelbosc-transit' ); ?></p>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Low Emission Zones (ZBE)', 'fontdelbosc-transit' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="fontdelbosc-transit-zbe-ttl"><?php esc_html_e( 'Cache lifetime (seconds)', 'fontdelbosc-transit' ); ?></label></th>
<td>
<input type="number" id="fontdelbosc-transit-zbe-ttl" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[zbe_cache_ttl]" value="<?php echo esc_attr( (string) $fontdelbosc_options['zbe_cache_ttl'] ); ?>" min="3600" max="604800" step="3600" class="small-text" />
<p class="description"><?php esc_html_e( 'ZBE rules change rarely; 86400 (24 hours) is a sensible default.', 'fontdelbosc-transit' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Zones shown', 'fontdelbosc-transit' ); ?></th>
<td>
<fieldset>
<?php
$fontdelbosc_zones = (array) $fontdelbosc_options['zbe_municipalities'];
$fontdelbosc_index_list = is_array( $fontdelbosc_index ) ? $fontdelbosc_index : Fontdelbosc_Transit_Api_Zbe::DEFAULT_CATALAN_ZONES;
foreach ( $fontdelbosc_index_list as $fontdelbosc_slug ) :
$fontdelbosc_id = 'fontdelbosc-transit-zone-' . esc_attr( $fontdelbosc_slug );
?>
<label for="<?php echo esc_attr( $fontdelbosc_id ); ?>">
<input type="checkbox" id="<?php echo esc_attr( $fontdelbosc_id ); ?>" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[zbe_municipalities][]" value="<?php echo esc_attr( $fontdelbosc_slug ); ?>" <?php checked( in_array( $fontdelbosc_slug, $fontdelbosc_zones, true ) ); ?> />
<?php echo esc_html( preg_replace( '/(?<!^)[A-Z]/', ' $0', $fontdelbosc_slug ) ?? $fontdelbosc_slug ); ?>
</label><br />
<?php endforeach; ?>
<p class="description"><?php esc_html_e( 'Catalan zones are pre-selected. When the DGT publishes new zones they appear here automatically.', 'fontdelbosc-transit' ); ?></p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( '2026 notice', 'fontdelbosc-transit' ); ?></th>
<td>
<label for="fontdelbosc-transit-2026"><input type="checkbox" id="fontdelbosc-transit-2026" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[show_zbe_2026_notice]" value="1" <?php checked( $fontdelbosc_options['show_zbe_2026_notice'] ); ?> /> <?php esc_html_e( 'Show the etiqueta B 2026 restriction notice in the ZBE view.', 'fontdelbosc-transit' ); ?></label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Data on uninstall', 'fontdelbosc-transit' ); ?></th>
<td>
<label for="fontdelbosc-transit-delete"><input type="checkbox" id="fontdelbosc-transit-delete" name="<?php echo esc_attr( Fontdelbosc_Transit_Plugin::option_name() ); ?>[delete_data_on_uninstall]" value="1" <?php checked( $fontdelbosc_options['delete_data_on_uninstall'] ); ?> /> <?php esc_html_e( 'Delete options and cached data when the plugin is uninstalled.', 'fontdelbosc-transit' ); ?></label>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<h2><?php esc_html_e( 'Status', 'fontdelbosc-transit' ); ?></h2>
<table class="form-table" role="presentation">
<?php if ( isset( $fontdelbosc_status['incidents'] ) ) : ?>
<tr>
<th scope="row"><?php esc_html_e( 'Incidents', 'fontdelbosc-transit' ); ?></th>
<td>
<?php
printf(
/* translators: 1: date of the last fetch, 2: source, 3: incident count. */
esc_html__( 'Last fetch: %1$s (%2$s source, %3$s incidents).', 'fontdelbosc-transit' ),
esc_html( Fontdelbosc_Transit_Maps::format_time( gmdate( 'Y-m-d H:i:s', (int) $fontdelbosc_status['incidents']['last_fetch'] ) . ' UTC' ) ),
esc_html( (string) $fontdelbosc_status['incidents']['source'] ),
esc_html( (string) $fontdelbosc_status['incidents']['count'] )
);
?>
</td>
</tr>
<?php endif; ?>
<?php if ( isset( $fontdelbosc_status['zbe'] ) ) : ?>
<tr>
<th scope="row"><?php esc_html_e( 'Low Emission Zones (ZBE)', 'fontdelbosc-transit' ); ?></th>
<td>
<?php
printf(
/* translators: 1: date of the last fetch, 2: zone count. */
esc_html__( 'Last fetch: %1$s (%2$s zones loaded).', 'fontdelbosc-transit' ),
esc_html( Fontdelbosc_Transit_Maps::format_time( gmdate( 'Y-m-d H:i:s', (int) $fontdelbosc_status['zbe']['last_fetch'] ) . ' UTC' ) ),
esc_html( (string) $fontdelbosc_status['zbe']['zones'] )
);
?>
</td>
</tr>
<?php endif; ?>
</table>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="fontdelbosc_transit_flush" />
<?php wp_nonce_field( 'fontdelbosc_transit_flush' ); ?>
<?php submit_button( __( 'Flush cache', 'fontdelbosc-transit' ), 'secondary', 'submit', false ); ?>
</form>
<h2><?php esc_html_e( 'Documentation', 'fontdelbosc-transit' ); ?></h2>
<h3><?php esc_html_e( 'Shortcode', 'fontdelbosc-transit' ); ?></h3>
<table class="widefat striped" role="presentation">
<thead>
<tr><th><?php esc_html_e( 'Attribute', 'fontdelbosc-transit' ); ?></th><th><?php esc_html_e( 'Default', 'fontdelbosc-transit' ); ?></th><th><?php esc_html_e( 'Values', 'fontdelbosc-transit' ); ?></th></tr>
</thead>
<tbody>
<tr><td><code>view</code></td><td><code>c15</code></td><td><code>c15</code>, <code>all</code>, <code>zbe</code></td></tr>
<tr><td><code>max</code></td><td><?php esc_html_e( 'Setting above', 'fontdelbosc-transit' ); ?></td><td>1–50</td></tr>
<tr><td><code>comarca</code></td><td>—</td><td><code>altpenedes</code>, <code>anoia</code>, <code>baixllobregat</code>, <code>garraf</code> — <?php esc_html_e( 'comma-separated list allowed', 'fontdelbosc-transit' ); ?></td></tr>
</tbody>
</table>
<p>
<code>[fontdelbosc_transit]</code> ·
<code>[fontdelbosc_transit view="all" max="10"]</code> ·
<code>[fontdelbosc_transit view="all" comarca="anoia"]</code> ·
<code>[fontdelbosc_transit view="zbe"]</code>
</p>
<h3><?php esc_html_e( 'Widget', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'A sidebar widget with an editable title shows the five latest C-15 incidents or a summary of the selected Low Emission Zones.', 'fontdelbosc-transit' ); ?></p>
<h3><?php esc_html_e( 'Data sources', 'fontdelbosc-transit' ); ?></h3>
<ul>
<li><?php esc_html_e( 'Incidents: Servei Català de Trànsit via the DGT national access point (DATEX2), enriched with the SCT public RSS feed, which provides the incident text and links.', 'fontdelbosc-transit' ); ?></li>
<li><?php esc_html_e( 'Low Emission Zones: DGT national access point (DATEX2 v3), with the zone list taken from the live index.', 'fontdelbosc-transit' ); ?></li>
<li><?php esc_html_e( 'Attribution is mandatory and cannot be disabled: Incidències — Servei Català de Trànsit (SCT) / CIVICAT; ZBE — DGT / Punt d\'Accés Nacional.', 'fontdelbosc-transit' ); ?></li>
</ul>
<h3><?php esc_html_e( 'Caching', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'Incident data is cached for the lifetime set above (5 minutes by default) and zone rules for 24 hours. When a source fails, the last cached data is served instead (stale-while-error) and the footer shows the age of the data using the source\'s own publication timestamp.', 'fontdelbosc-transit' ); ?></p>
<p><?php esc_html_e( 'Use the Flush cache button above to force a fresh fetch of everything.', 'fontdelbosc-transit' ); ?></p>
<h3><?php esc_html_e( 'Data, privacy and uninstall', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'The plugin stores no personal data and no secrets: only the settings entry, a small fetch-status entry, and transients holding the cached public data. No API keys are needed.', 'fontdelbosc-transit' ); ?></p>
<p><?php esc_html_e( 'By default, uninstalling keeps the stored data. With "Delete options and cached data when the plugin is uninstalled" enabled, uninstalling removes the settings, the status entry and every cached transient — nothing is left behind.', 'fontdelbosc-transit' ); ?></p>
<h3><?php esc_html_e( 'Security', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'No user input reaches the external endpoints: all URLs are hardcoded constants. The only state-changing admin action requires the manage_options capability and a nonce. All output is escaped, and the XML responses are parsed with external entity loading disabled.', 'fontdelbosc-transit' ); ?></p>
<h3><?php esc_html_e( 'Compatibility', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'WordPress 6.4 – 7.1 and PHP 8.2 – 8.5, single site (Multisite is not supported).', 'fontdelbosc-transit' ); ?></p>
<h3><?php esc_html_e( 'The 2026 etiqueta B rule', 'fontdelbosc-transit' ); ?></h3>
<p><?php esc_html_e( 'From 1 January 2026, label B vehicles are banned in Catalan ZBEs when high-pollution protocols are activated. The persistent notice in the ZBE view can be disabled with the toggle above.', 'fontdelbosc-transit' ); ?></p>
</div>