Release v0.6.1 - Bug fixes and enhancements
All checks were successful
Create Release Package / build-release (push) Successful in 1m1s

New Features:
- Auto-update system with configurable check frequency
- Updates tab in settings with manual check button
- Localhost development mode bypasses license validation
- Extended general settings (address, contact, social media)
- Pricing settings split into subtabs
- Guest ID/passport encryption using AES-256-CBC
- Guest auto-creation from booking form

Bug Fixes:
- Fixed Booking admin issues with auto-draft status
- Fixed guest dropdown loading in booking form
- Fixed booking history display on Guest edit page
- Fixed service pricing meta box (Gutenberg hiding meta boxes)

Changes:
- Admin submenu reordered for better organization
- Booking title shows guest name and dates (room removed)
- Service, Guest, Booking use classic editor (not Gutenberg)
- Settings tabs flush with content (no gap)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 15:18:27 +01:00
parent c17dd53c5a
commit 13ba264431
11 changed files with 1699 additions and 173 deletions

473
src/License/Updater.php Normal file
View File

@@ -0,0 +1,473 @@
<?php
/**
* Plugin Updater class.
*
* Integrates with WordPress plugin update system to check for and install
* updates from the license server.
*
* @package Magdev\WpBnb\License
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\License;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\LicenseClient;
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
use Symfony\Component\HttpClient\HttpClient;
/**
* Handles plugin auto-updates from the license server.
*/
final class Updater {
/**
* Singleton instance.
*
* @var Updater|null
*/
private static ?Updater $instance = null;
/**
* Plugin basename (e.g., wp-bnb/wp-bnb.php).
*
* @var string
*/
private string $plugin_basename;
/**
* Plugin slug.
*
* @var string
*/
private string $plugin_slug;
/**
* Current plugin version.
*
* @var string
*/
private string $current_version;
/**
* Cache key for update info.
*
* @var string
*/
private const CACHE_KEY = 'wp_bnb_update_info';
/**
* Cache key for last check timestamp.
*
* @var string
*/
private const LAST_CHECK_KEY = 'wp_bnb_update_last_check';
/**
* Default cache duration in seconds (12 hours).
*
* @var int
*/
private const DEFAULT_CHECK_FREQUENCY = 12;
// Option keys for update settings.
public const OPTION_NOTIFICATIONS_ENABLED = 'wp_bnb_update_notifications_enabled';
public const OPTION_AUTO_INSTALL_ENABLED = 'wp_bnb_auto_install_enabled';
public const OPTION_CHECK_FREQUENCY = 'wp_bnb_update_check_frequency';
/**
* License client instance.
*
* @var SecureLicenseClient|LicenseClient|null
*/
private SecureLicenseClient|LicenseClient|null $client = null;
/**
* Constructor.
*
* @param string $plugin_file Full path to the main plugin file.
* @param string $current_version Current plugin version.
*/
public function __construct( string $plugin_file, string $current_version ) {
$this->plugin_basename = plugin_basename( $plugin_file );
$this->plugin_slug = dirname( $this->plugin_basename );
$this->current_version = $current_version;
self::$instance = $this;
}
/**
* Get the singleton instance.
*
* @return Updater|null
*/
public static function get_instance(): ?Updater {
return self::$instance;
}
/**
* Initialize update hooks.
*
* @return void
*/
public function init(): void {
// Allow complete disable via constant.
if ( defined( 'WP_BNB_DISABLE_AUTO_UPDATE' ) && WP_BNB_DISABLE_AUTO_UPDATE ) {
return;
}
// Hook into WordPress update system.
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
add_action( 'upgrader_process_complete', array( $this, 'after_update' ), 10, 2 );
// Auto-install filter for WordPress background updates.
add_filter( 'auto_update_plugin', array( $this, 'auto_update_plugin' ), 10, 2 );
// Clear update cache when license settings change.
add_action( 'update_option_' . Manager::OPTION_LICENSE_KEY, array( $this, 'clear_cache' ) );
add_action( 'update_option_' . Manager::OPTION_SERVER_URL, array( $this, 'clear_cache' ) );
// AJAX handler for manual update check.
add_action( 'wp_ajax_wp_bnb_check_updates', array( $this, 'ajax_check_updates' ) );
}
/**
* Check if update notifications are enabled.
*
* @return bool
*/
public static function is_notifications_enabled(): bool {
return 'yes' === get_option( self::OPTION_NOTIFICATIONS_ENABLED, 'yes' );
}
/**
* Check if auto-install is enabled.
*
* @return bool
*/
public static function is_auto_install_enabled(): bool {
return 'yes' === get_option( self::OPTION_AUTO_INSTALL_ENABLED, 'no' );
}
/**
* Get the update check frequency in hours.
*
* @return int
*/
public static function get_check_frequency(): int {
$frequency = (int) get_option( self::OPTION_CHECK_FREQUENCY, self::DEFAULT_CHECK_FREQUENCY );
// Clamp between 1 and 168 hours (1 week).
return max( 1, min( 168, $frequency ) );
}
/**
* Get cache duration in seconds based on check frequency.
*
* @return int
*/
private function get_cache_duration(): int {
return self::get_check_frequency() * 3600;
}
/**
* Filter for WordPress auto-update system.
*
* @param bool|null $update Whether to update the plugin.
* @param object $item The plugin update object.
* @return bool|null
*/
public function auto_update_plugin( $update, object $item ) {
// Only affect our plugin.
if ( ! isset( $item->plugin ) || $item->plugin !== $this->plugin_basename ) {
return $update;
}
// Check if auto-install is enabled and license is valid.
if ( self::is_auto_install_enabled() && Manager::is_license_valid() ) {
return true;
}
return $update;
}
/**
* Get current plugin version.
*
* @return string
*/
public function get_current_version(): string {
return $this->current_version;
}
/**
* Get last update check timestamp.
*
* @return int
*/
public static function get_last_check(): int {
return (int) get_option( self::LAST_CHECK_KEY, 0 );
}
/**
* AJAX handler: Check for updates.
*
* @return void
*/
public function ajax_check_updates(): void {
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
if ( ! current_user_can( 'update_plugins' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to check for updates.', 'wp-bnb' ),
) );
}
$update_info = $this->get_cached_update_info( true );
if ( null === $update_info ) {
wp_send_json_success( array(
'update_available' => false,
'current_version' => $this->current_version,
'message' => __( 'Could not check for updates. Please verify your license configuration.', 'wp-bnb' ),
) );
}
$response = array(
'update_available' => $update_info->updateAvailable && version_compare( $this->current_version, $update_info->version ?? '', '<' ),
'current_version' => $this->current_version,
'latest_version' => $update_info->version ?? $this->current_version,
'last_check' => time(),
);
if ( $response['update_available'] ) {
$response['message'] = sprintf(
/* translators: %s: New version number */
__( 'A new version (%s) is available.', 'wp-bnb' ),
$update_info->version
);
$response['changelog'] = $update_info->changelog ?? '';
} else {
$response['message'] = __( 'You are running the latest version.', 'wp-bnb' );
}
wp_send_json_success( $response );
}
/**
* Initialize the license client.
*
* @return bool
*/
private function init_client(): bool {
if ( null !== $this->client ) {
return true;
}
$server_url = Manager::get_server_url();
$server_secret = Manager::get_server_secret();
if ( empty( $server_url ) ) {
return false;
}
try {
if ( ! empty( $server_secret ) ) {
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $server_url,
serverSecret: $server_secret,
);
} else {
$this->client = new LicenseClient(
httpClient: HttpClient::create(),
baseUrl: $server_url,
);
}
return true;
} catch ( \Throwable $e ) {
return false;
}
}
/**
* Check for plugin updates.
*
* @param object $transient The update_plugins transient.
* @return object Modified transient.
*/
public function check_for_updates( object $transient ): object {
if ( empty( $transient->checked ) ) {
return $transient;
}
// Respect notifications enabled setting.
if ( ! self::is_notifications_enabled() ) {
return $transient;
}
$update_info = $this->get_update_info();
if ( null === $update_info || ! $update_info->updateAvailable ) {
return $transient;
}
// Compare versions.
if ( version_compare( $this->current_version, $update_info->version ?? '', '>=' ) ) {
return $transient;
}
// Add to update response.
$transient->response[ $this->plugin_basename ] = (object) array(
'slug' => $update_info->slug ?? $this->plugin_slug,
'plugin' => $this->plugin_basename,
'new_version' => $update_info->version,
'url' => $update_info->homepage ?? '',
'package' => $update_info->downloadUrl,
'icons' => $update_info->icons ?? array(),
'tested' => $update_info->tested ?? '',
'requires' => $update_info->requires ?? '',
'requires_php' => $update_info->requiresPhp ?? '',
);
return $transient;
}
/**
* Provide plugin information for the details modal.
*
* @param false|object|array $result The result object or array.
* @param string $action The API action being performed.
* @param object $args Plugin API arguments.
* @return false|object
*/
public function plugin_info( $result, string $action, object $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
if ( ! isset( $args->slug ) || $args->slug !== $this->plugin_slug ) {
return $result;
}
$update_info = $this->get_update_info();
if ( null === $update_info ) {
return $result;
}
$plugin_info = (object) array(
'name' => $update_info->name ?? 'WP BnB Manager',
'slug' => $update_info->slug ?? $this->plugin_slug,
'version' => $update_info->version ?? $this->current_version,
'author' => '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>',
'homepage' => $update_info->homepage ?? 'https://src.bundespruefstelle.ch/magdev/wp-bnb',
'requires' => $update_info->requires ?? '6.0',
'tested' => $update_info->tested ?? '',
'requires_php' => $update_info->requiresPhp ?? '8.3',
'last_updated' => $update_info->lastUpdated?->format( 'Y-m-d' ) ?? '',
'download_link' => $update_info->downloadUrl ?? '',
'sections' => $update_info->sections ?? array(
'description' => __( 'A comprehensive Bed & Breakfast management plugin for WordPress.', 'wp-bnb' ),
'changelog' => $update_info->changelog ?? '',
),
);
if ( ! empty( $update_info->icons ) ) {
$plugin_info->icons = $update_info->icons;
}
return $plugin_info;
}
/**
* Clear update cache after upgrade.
*
* @param \WP_Upgrader $upgrader WP_Upgrader instance.
* @param array $hook_extra Extra arguments passed to hooked filters.
* @return void
*/
public function after_update( \WP_Upgrader $upgrader, array $hook_extra ): void {
if ( ! isset( $hook_extra['plugins'] ) || ! is_array( $hook_extra['plugins'] ) ) {
return;
}
if ( in_array( $this->plugin_basename, $hook_extra['plugins'], true ) ) {
$this->clear_cache();
}
}
/**
* Get update info from cache or server.
*
* @param bool $force_refresh Force refresh from server.
* @return UpdateInfo|null
*/
public function get_cached_update_info( bool $force_refresh = false ): ?UpdateInfo {
if ( ! $force_refresh ) {
$cached = get_transient( self::CACHE_KEY );
if ( false !== $cached && $cached instanceof UpdateInfo ) {
return $cached;
}
}
// Check if license is configured.
$license_key = Manager::get_license_key();
if ( empty( $license_key ) ) {
return null;
}
if ( ! $this->init_client() ) {
return null;
}
try {
$domain = $this->get_current_domain();
$update_info = $this->client->checkForUpdates(
licenseKey: $license_key,
domain: $domain,
pluginSlug: $this->plugin_slug,
currentVersion: $this->current_version,
);
// Cache the result and update last check timestamp.
set_transient( self::CACHE_KEY, $update_info, $this->get_cache_duration() );
update_option( self::LAST_CHECK_KEY, time() );
return $update_info;
} catch ( \Throwable $e ) {
// Silently fail and return null - don't break WordPress.
return null;
}
}
/**
* Get update info from cache or server (alias for WordPress update system).
*
* @param bool $force_refresh Force refresh from server.
* @return UpdateInfo|null
*/
private function get_update_info( bool $force_refresh = false ): ?UpdateInfo {
return $this->get_cached_update_info( $force_refresh );
}
/**
* Get current domain.
*
* @return string
*/
private function get_current_domain(): string {
$site_url = get_site_url();
$parsed = wp_parse_url( $site_url );
return $parsed['host'] ?? '';
}
/**
* Clear the update cache.
*
* @return void
*/
public function clear_cache(): void {
delete_transient( self::CACHE_KEY );
}
}