From 13ba26443102a9eb0fa147a4d23a6aced2393dcd Mon Sep 17 00:00:00 2001 From: magdev Date: Tue, 3 Feb 2026 15:18:27 +0100 Subject: [PATCH] Release v0.6.1 - Bug fixes and enhancements 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 --- CHANGELOG.md | 69 ++++ README.md | 65 +++- assets/css/admin.css | 54 ++- assets/js/admin.js | 95 ++++- src/License/Manager.php | 47 +++ src/License/Updater.php | 473 +++++++++++++++++++++++++ src/Plugin.php | 712 ++++++++++++++++++++++++++++++++------ src/PostTypes/Booking.php | 214 +++++++++--- src/PostTypes/Guest.php | 122 ++++++- src/PostTypes/Service.php | 17 + wp-bnb.php | 4 +- 11 files changed, 1699 insertions(+), 173 deletions(-) create mode 100644 src/License/Updater.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f3bcf..1cfaaa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,74 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.1] - 2026-02-03 + +### Added + +- Auto-Update System: + - New `src/License/Updater.php` class for WordPress update integration + - Hooks into `pre_set_site_transient_update_plugins` for update detection + - Plugin info modal via `plugins_api` filter + - Configurable update check frequency (1-168 hours) + - Option to enable/disable update notifications + - Option to enable/disable automatic updates + - AJAX endpoint for manual update check + - Automatic cache clearing when license settings change +- Updates Tab in Settings: + - Enable/disable update notifications toggle + - Enable/disable automatic updates toggle + - Update check frequency setting + - Manual "Check for Updates" button + - Display of last check timestamp and current version +- Localhost Development Mode: + - License bypass for local development environments + - Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains + - Private IP range detection (10.x.x.x, 172.16-31.x.x, 192.168.x.x) + - "Development Mode" notice on Dashboard and License settings page +- Extended General Settings: + - Business address fields (street, city, postal code, country) + - Contact fields (email, phone, website) + - Social media fields (Facebook, Instagram, X/Twitter, LinkedIn, TripAdvisor) +- Pricing Settings Subtabs: + - Split into three subtabs: Pricing Tiers, Weekend Days, Seasons + - Each subtab has its own save button + - Seasons subtab shows priority column and link to Seasons Manager +- Guest Data Encryption: + - AES-256-CBC encryption for sensitive data (ID/passport numbers) + - Uses WordPress AUTH_KEY for encryption key derivation + - `encrypt()` and `decrypt()` methods in Guest class + - Backward compatible with legacy unencrypted data + - Security notice displayed in Identification meta box +- Guest Auto-Creation from Booking: + - When new guest data is entered in booking form, guest record is automatically created + - Links booking to the new guest via guest_id meta + - Prevents duplicate guest entries + +### Changed + +- Admin submenu reordered for better organization: + - Dashboard at top, Settings at bottom + - Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons +- Booking title auto-generates with guest name and dates (room number removed) +- Disabled Gutenberg block editor for form-based post types: + - Service, Guest, and Booking now use classic editor + - Meta boxes display properly instead of being hidden at bottom + - Form-based interfaces more appropriate than block editor for data entry +- Settings tabs now flush with tab content (no gap) + +### Fixed + +- Fixed Booking admin issues with auto-draft status causing type errors +- Fixed guest dropdown to always load existing guests +- Fixed booking history display on Guest edit page +- Fixed service pricing meta box not displaying radio buttons (Gutenberg hiding meta boxes) + +### Security + +- Guest ID/passport numbers encrypted at rest using AES-256-CBC +- Random IV generation for each encryption operation +- Secure key derivation from WordPress AUTH_KEY + ## [0.6.0] - 2026-02-02 ### Added @@ -358,6 +426,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Input sanitization and output escaping - Server secret masking in license settings +[0.6.1]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.1 [0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0 [0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0 [0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0 diff --git a/README.md b/README.md index 5fc6b15..2330018 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,15 @@ WP BnB Management enables WordPress to act as a full management system for B&B h - **Multi-Property Support**: Manage multiple buildings, each with multiple rooms - **Flexible Pricing**: Configure short-term (nights), mid-term (weeks), and long-term (months) pricing +- **Seasonal Pricing**: Set price modifiers for high/low seasons - **Booking Management**: Track reservations from inquiry to checkout - **Guest Management**: Store guest information securely with GDPR compliance +- **Data Encryption**: Sensitive guest data (ID/passport) encrypted at rest - **Additional Services**: Offer extras like breakfast, parking, or tours - **Frontend Integration**: Gutenberg blocks, widgets, and shortcodes -- **Contact Form 7 Integration**: Accept booking requests through forms +- **Auto-Updates**: Automatic update checks and installation from license server +- **Development Mode**: License bypass for local development environments +- **Contact Form 7 Integration**: Accept booking requests through forms (planned) ### Requirements @@ -44,6 +48,23 @@ WP BnB Management enables WordPress to act as a full management system for B&B h - **Business Name**: Your B&B business name - **Currency**: Select your preferred currency (CHF, EUR, USD, GBP) +- **Business Address**: Street, city, postal code, country +- **Contact Information**: Email, phone, website +- **Social Media**: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor + +### Update Settings + +- **Update Notifications**: Enable/disable update notifications in WordPress +- **Automatic Updates**: Enable/disable automatic plugin updates +- **Check Frequency**: How often to check for updates (1-168 hours) + +### Development Mode + +The plugin automatically detects local development environments and bypasses license validation. Supported environments: + +- localhost, 127.0.0.1, ::1 +- Domains ending in .local, .test, .localhost, .dev, .ddev.site +- Private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) ## Usage @@ -81,26 +102,46 @@ WP BnB Management enables WordPress to act as a full management system for B&B h Display buildings and rooms on your site using shortcodes: ```txt -[wp_bnb_buildings] -[wp_bnb_rooms building="123"] -[wp_bnb_room_search] +[bnb_buildings] - List all buildings (grid/list layout) +[bnb_rooms building="123"] - List rooms, optionally filtered by building +[bnb_room_search] - Interactive room search form +[bnb_building id="123"] - Display a single building +[bnb_room id="456"] - Display a single room with availability ``` +### Shortcode Attributes + +**`[bnb_buildings]`** and **`[bnb_rooms]`**: + +- `layout` - "grid" or "list" (default: grid) +- `columns` - 1-4 columns (default: 3) +- `limit` - Number of items (default: 12) +- `orderby` - title, date, price, capacity (default: title) +- `order` - ASC or DESC (default: ASC) + +**`[bnb_rooms]`** additional attributes: + +- `building` - Building ID to filter by +- `room_type` - Room type slug to filter by +- `amenities` - Comma-separated amenity slugs + ## Gutenberg Blocks The following blocks are available in the block editor: -- **Building** - Display a single building -- **Room** - Display a single room -- **Room Search** - Search and filter rooms -- **Booking Form** - Accept booking requests +- **Building** - Display a single building with details +- **Room** - Display a single room with availability form +- **Room Search** - Interactive search form with filters +- **Buildings List** - Display buildings grid/list +- **Rooms List** - Display rooms grid/list with filters ## Widgets Available sidebar widgets: -- **Similar Rooms** - Show rooms similar to the current one +- **Similar Rooms** - Show rooms from same building or room type - **Building Rooms** - List all rooms in a building +- **Availability Calendar** - Mini calendar showing booking status ## Hooks and Filters @@ -123,7 +164,7 @@ add_action( 'wp_bnb_before_booking_create', function( $booking_data ) { ### Do I need a license to use this plugin? -Yes, a valid license is required to use the frontend features. The admin functionality works without a license for evaluation purposes. +Yes, a valid license is required to use the frontend features in production. The admin functionality works without a license for evaluation purposes. Local development environments (localhost, .local, .test, .dev domains) automatically bypass license validation. ### Can I manage multiple properties? @@ -137,6 +178,10 @@ Yes, guest data can be exported and deleted on request, and consent is tracked a WooCommerce integration for payments is planned for a future release. +### How is guest data secured? + +Sensitive guest data like passport/ID numbers are encrypted using AES-256-CBC encryption before storage. The encryption key is derived from your WordPress AUTH_KEY, ensuring data is secure at rest. + ## Changelog See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes. diff --git a/assets/css/admin.css b/assets/css/admin.css index d807e7e..e8294c4 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -54,7 +54,8 @@ /* Settings Tabs */ .nav-tab-wrapper { - margin-bottom: 20px; + margin-bottom: 0; + border-bottom: 1px solid #c3c4c7; } .tab-content { @@ -64,6 +65,57 @@ padding: 20px; } +/* Settings Subtabs */ +.wp-bnb-subtabs { + display: flex; + gap: 0; + margin-bottom: 20px; + border-bottom: 1px solid #c3c4c7; +} + +.wp-bnb-subtab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 16px; + text-decoration: none; + color: #50575e; + background: #f6f7f7; + border: 1px solid #c3c4c7; + border-bottom: none; + margin-bottom: -1px; + margin-right: -1px; + font-size: 13px; + transition: background 0.15s ease, color 0.15s ease; +} + +.wp-bnb-subtab:first-child { + border-top-left-radius: 4px; +} + +.wp-bnb-subtab:last-child { + border-top-right-radius: 4px; + margin-right: 0; +} + +.wp-bnb-subtab:hover { + background: #fff; + color: #135e96; +} + +.wp-bnb-subtab.active { + background: #fff; + color: #1d2327; + font-weight: 600; + border-bottom-color: #fff; +} + +.wp-bnb-subtab .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + /* Form Tables */ .form-table th { width: 200px; diff --git a/assets/js/admin.js b/assets/js/admin.js index 4f0a71b..6738259 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -91,6 +91,91 @@ } } + /** + * Initialize update check functionality. + */ + function initUpdateCheck() { + var $checkBtn = $('#wp-bnb-check-updates'); + var $spinner = $('#wp-bnb-update-spinner'); + var $message = $('#wp-bnb-update-message'); + var $latestVersion = $('#wp-bnb-latest-version'); + var $lastCheck = $('#wp-bnb-update-last-check'); + + if (!$checkBtn.length) { + return; + } + + $checkBtn.on('click', function(e) { + e.preventDefault(); + + // Disable button and show spinner. + $checkBtn.prop('disabled', true); + $spinner.addClass('is-active'); + $message.hide(); + + $.ajax({ + url: wpBnbAdmin.ajaxUrl, + type: 'POST', + data: { + action: 'wp_bnb_check_updates', + nonce: wpBnbAdmin.nonce + }, + success: function(response) { + $spinner.removeClass('is-active'); + $checkBtn.prop('disabled', false); + + if (response.success) { + var data = response.data; + + // Update last check time. + $lastCheck.text(wpBnbAdmin.i18n.justNow || 'Just now'); + + // Update version display. + if (data.update_available) { + $latestVersion.html( + '' + + data.latest_version + + ' ' + + ' ' + + '' + (wpBnbAdmin.i18n.updateAvailable || 'Update available!') + '' + ); + showUpdateMessage('success', data.message); + } else { + $latestVersion.html( + data.latest_version + + ' ' + + (wpBnbAdmin.i18n.upToDate || '(You are up to date)') + + '' + ); + showUpdateMessage('success', data.message); + } + } else { + showUpdateMessage('error', response.data.message || wpBnbAdmin.i18n.error); + } + }, + error: function() { + $spinner.removeClass('is-active'); + $checkBtn.prop('disabled', false); + showUpdateMessage('error', wpBnbAdmin.i18n.error); + } + }); + }); + + /** + * Show an update message. + * + * @param {string} type Message type (success or error). + * @param {string} message Message text. + */ + function showUpdateMessage(type, message) { + $message + .removeClass('success error') + .addClass(type) + .text(message) + .fadeIn(); + } + } + /** * Initialize room gallery functionality. */ @@ -768,7 +853,7 @@ return; } - $pricingTypeInputs.on('change', function() { + function updatePriceRowVisibility() { var pricingType = $('input[name="bnb_service_pricing_type"]:checked').val(); if (pricingType === 'included') { @@ -784,7 +869,12 @@ $priceDescription.text(wpBnbAdmin.i18n.perBookingDescription || 'This price will be charged once for the booking.'); } } - }); + } + + $pricingTypeInputs.on('change', updatePriceRowVisibility); + + // Set initial visibility state on page load. + updatePriceRowVisibility(); } /** @@ -937,6 +1027,7 @@ // Initialize on document ready. $(document).ready(function() { initLicenseManagement(); + initUpdateCheck(); initRoomGallery(); initPricingSettings(); initSeasonForm(); diff --git a/src/License/Manager.php b/src/License/Manager.php index bbc6576..2e3b0f0 100644 --- a/src/License/Manager.php +++ b/src/License/Manager.php @@ -126,13 +126,60 @@ final class Manager { /** * Check if license is valid. * + * Localhost environments bypass the license check to allow + * full functionality during development. + * * @return bool */ public static function is_license_valid(): bool { + // Bypass license check for localhost environments. + if ( self::is_localhost() ) { + return true; + } + $status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); return 'valid' === $status; } + /** + * Check if running on localhost. + * + * Detects common local development environments: + * - localhost / 127.0.0.1 / ::1 + * - .local, .test, .localhost domains + * - Private IP ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + * + * @return bool + */ + public static function is_localhost(): bool { + $site_url = get_site_url(); + $parsed = wp_parse_url( $site_url ); + $host = $parsed['host'] ?? ''; + + // Check for localhost variations. + if ( in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) ) { + return true; + } + + // Check for common local development TLDs. + $local_tlds = array( '.local', '.test', '.localhost', '.dev', '.ddev.site' ); + foreach ( $local_tlds as $tld ) { + if ( str_ends_with( $host, $tld ) ) { + return true; + } + } + + // Check for private IP ranges. + if ( filter_var( $host, FILTER_VALIDATE_IP ) ) { + // 10.x.x.x, 172.16-31.x.x, 192.168.x.x. + if ( ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE ) ) { + return true; + } + } + + return false; + } + /** * Get license key. * diff --git a/src/License/Updater.php b/src/License/Updater.php new file mode 100644 index 0000000..090270b --- /dev/null +++ b/src/License/Updater.php @@ -0,0 +1,473 @@ +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' => 'Marco Graetsch', + '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 ); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index d76bc87..144e132 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -20,6 +20,8 @@ use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar; use Magdev\WpBnb\Frontend\Widgets\BuildingRooms; use Magdev\WpBnb\Frontend\Widgets\SimilarRooms; use Magdev\WpBnb\License\Manager as LicenseManager; +use Magdev\WpBnb\License\Updater as LicenseUpdater; +use Magdev\WcLicensedProductClient\Dto\UpdateInfo; use Magdev\WpBnb\PostTypes\Booking; use Magdev\WpBnb\PostTypes\Building; use Magdev\WpBnb\PostTypes\Guest; @@ -128,6 +130,9 @@ final class Plugin { // Initialize License Manager (always active for admin). LicenseManager::get_instance(); + // Initialize auto-updater (requires license configuration). + $this->init_updater(); + // Initialize admin components. if ( is_admin() ) { $this->init_admin(); @@ -139,6 +144,19 @@ final class Plugin { } } + /** + * Initialize the plugin auto-updater. + * + * @return void + */ + private function init_updater(): void { + $updater = new LicenseUpdater( + plugin_file: WP_BNB_PATH . 'wp-bnb.php', + current_version: WP_BNB_VERSION, + ); + $updater->init(); + } + /** * Initialize admin components. * @@ -147,6 +165,7 @@ final class Plugin { private function init_admin(): void { // Admin menu and settings will be added here. add_action( 'admin_menu', array( $this, 'register_admin_menu' ) ); + add_action( 'admin_menu', array( $this, 'reorder_admin_menu' ), 99 ); add_action( 'admin_init', array( $this, 'register_settings' ) ); // Initialize seasons admin page. @@ -280,6 +299,10 @@ final class Plugin { 'guestBlocked' => __( 'Blocked', 'wp-bnb' ), 'perNightDescription' => __( 'This price will be charged per night of the stay.', 'wp-bnb' ), 'perBookingDescription' => __( 'This price will be charged once for the booking.', 'wp-bnb' ), + 'justNow' => __( 'Just now', 'wp-bnb' ), + 'updateAvailable' => __( 'Update available!', 'wp-bnb' ), + 'upToDate' => __( '(You are up to date)', 'wp-bnb' ), + 'checkingUpdates' => __( 'Checking for updates...', 'wp-bnb' ), ), ) ); @@ -392,6 +415,59 @@ final class Plugin { ); } + /** + * Reorder the admin submenu items. + * + * Places Dashboard at top, Settings at bottom, and organizes + * the remaining items in logical order. + * + * @return void + */ + public function reorder_admin_menu(): void { + global $submenu; + + if ( ! isset( $submenu['wp-bnb'] ) ) { + return; + } + + // Define the desired order of menu slugs. + $desired_order = array( + 'wp-bnb', // Dashboard. + 'edit.php?post_type=bnb_building', // Buildings. + 'edit.php?post_type=bnb_room', // Rooms. + 'edit.php?post_type=bnb_booking', // Bookings. + 'edit.php?post_type=bnb_guest', // Guests. + 'edit.php?post_type=bnb_service', // Services. + 'wp-bnb-calendar', // Calendar. + 'wp-bnb-seasons', // Seasons. + 'wp-bnb-settings', // Settings (always last). + ); + + $current_menu = $submenu['wp-bnb']; + $ordered_menu = array(); + $index = 0; + + // Add items in the desired order. + foreach ( $desired_order as $slug ) { + foreach ( $current_menu as $key => $item ) { + if ( $item[2] === $slug ) { + $ordered_menu[ $index ] = $item; + unset( $current_menu[ $key ] ); + ++$index; + break; + } + } + } + + // Append any remaining items not in the desired order. + foreach ( $current_menu as $item ) { + $ordered_menu[ $index ] = $item; + ++$index; + } + + $submenu['wp-bnb'] = $ordered_menu; + } + /** * Register plugin settings. * @@ -408,12 +484,21 @@ final class Plugin { * @return void */ public function render_dashboard_page(): void { - $license_status = LicenseManager::get_cached_status(); + $license_valid = LicenseManager::is_license_valid(); + $is_localhost = LicenseManager::is_localhost(); ?>

- + +
+

+ + + +

+
+

"> + + +

@@ -475,6 +564,9 @@ final class Plugin { case 'license': $this->render_license_settings(); break; + case 'updates': + $this->render_updates_settings(); + break; default: $this->render_general_settings(); break; @@ -495,6 +587,8 @@ final class Plugin {
+

+ +

+ + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + +
__( 'Monday', 'wp-bnb' ), 2 => __( 'Tuesday', 'wp-bnb' ), 3 => __( 'Wednesday', 'wp-bnb' ), @@ -558,118 +791,155 @@ final class Plugin { 7 => __( 'Sunday', 'wp-bnb' ), ); $selected_days = array_map( 'intval', explode( ',', $weekend_days ) ); + + $base_url = admin_url( 'admin.php?page=wp-bnb-settings&tab=pricing' ); ?> + + + +
-

-

+ + +

+

- - - - - - - - - - - - - - - -

-

- - - - - - - - -

-

- ' . esc_html__( 'Seasons Manager', 'wp-bnb' ) . '' - ); - ?> -

- - - - - - - - - - - - - - - - - - - - - +
name ); ?>start_date . ' - ' . $season->end_date ); ?>getModifierLabel() ); ?> - active ) : ?> - - - - -
+ + + + + + + + + + + + - -

- - + + + + +

+

+ + + + + + + + + + + + +

+

+ +

+ +

+ + + + +

+ + + + + + + + + + + + + + + + + + + + + + + +
name ); ?>start_date . ' - ' . $season->end_date ); ?>getModifierLabel() ); ?>priority ); ?> + active ) : ?> + + + + +
+ +
+

+
+ + +
+ +
+

+ + + +

+
+ +

@@ -773,6 +1054,160 @@ final class Plugin { +

+ get_current_version(); + $last_check = LicenseUpdater::get_last_check(); + $update_info = $updater->get_cached_update_info(); + $update_available = false; + $latest_version = $current_version; + $notifications_enabled = LicenseUpdater::is_notifications_enabled(); + $auto_install_enabled = LicenseUpdater::is_auto_install_enabled(); + $check_frequency = LicenseUpdater::get_check_frequency(); + $license_valid = LicenseManager::is_license_valid(); + + if ( $update_info instanceof UpdateInfo && $update_info->updateAvailable ) { + $latest_version = $update_info->version ?? $current_version; + $update_available = version_compare( $current_version, $latest_version, '<' ); + } + ?> + + + +

+ + + + + + + + + + + + + + + + +

+ + + + + + + +

+ + + +

+ + + + + + + + + + + + + + + + +

+ +

+
+ save_license_settings(); break; + case 'updates': + $this->save_updates_settings(); + break; default: $this->save_general_settings(); break; @@ -853,6 +1291,7 @@ final class Plugin { * @return void */ private function save_general_settings(): void { + // Business Information. if ( isset( $_POST['wp_bnb_business_name'] ) ) { update_option( 'wp_bnb_business_name', sanitize_text_field( wp_unslash( $_POST['wp_bnb_business_name'] ) ) ); } @@ -860,6 +1299,35 @@ final class Plugin { update_option( 'wp_bnb_currency', sanitize_text_field( wp_unslash( $_POST['wp_bnb_currency'] ) ) ); } + // Address fields. + $address_fields = array( 'street', 'city', 'postal', 'country' ); + foreach ( $address_fields as $field ) { + $key = 'wp_bnb_address_' . $field; + if ( isset( $_POST[ $key ] ) ) { + update_option( $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ); + } + } + + // Contact fields. + if ( isset( $_POST['wp_bnb_contact_email'] ) ) { + update_option( 'wp_bnb_contact_email', sanitize_email( wp_unslash( $_POST['wp_bnb_contact_email'] ) ) ); + } + if ( isset( $_POST['wp_bnb_contact_phone'] ) ) { + update_option( 'wp_bnb_contact_phone', sanitize_text_field( wp_unslash( $_POST['wp_bnb_contact_phone'] ) ) ); + } + if ( isset( $_POST['wp_bnb_contact_website'] ) ) { + update_option( 'wp_bnb_contact_website', esc_url_raw( wp_unslash( $_POST['wp_bnb_contact_website'] ) ) ); + } + + // Social media fields. + $social_fields = array( 'facebook', 'instagram', 'x', 'linkedin', 'tripadvisor' ); + foreach ( $social_fields as $field ) { + $key = 'wp_bnb_social_' . $field; + if ( isset( $_POST[ $key ] ) ) { + update_option( $key, esc_url_raw( wp_unslash( $_POST[ $key ] ) ) ); + } + } + add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Settings saved.', 'wp-bnb' ), 'success' ); settings_errors( 'wp_bnb_settings' ); } @@ -912,6 +1380,34 @@ final class Plugin { settings_errors( 'wp_bnb_settings' ); } + /** + * Save updates settings. + * + * @return void + */ + private function save_updates_settings(): void { + $notifications_enabled = isset( $_POST['wp_bnb_update_notifications_enabled'] ) ? 'yes' : 'no'; + update_option( LicenseUpdater::OPTION_NOTIFICATIONS_ENABLED, $notifications_enabled ); + + $auto_install_enabled = isset( $_POST['wp_bnb_auto_install_enabled'] ) ? 'yes' : 'no'; + update_option( LicenseUpdater::OPTION_AUTO_INSTALL_ENABLED, $auto_install_enabled ); + + if ( isset( $_POST['wp_bnb_update_check_frequency'] ) ) { + $frequency = absint( $_POST['wp_bnb_update_check_frequency'] ); + $frequency = max( 1, min( 168, $frequency ) ); // Clamp between 1-168 hours. + update_option( LicenseUpdater::OPTION_CHECK_FREQUENCY, $frequency ); + + // Clear update cache when frequency changes so new frequency takes effect. + $updater = LicenseUpdater::get_instance(); + if ( null !== $updater ) { + $updater->clear_cache(); + } + } + + add_settings_error( 'wp_bnb_settings', 'settings_saved', __( 'Update settings saved.', 'wp-bnb' ), 'success' ); + settings_errors( 'wp_bnb_settings' ); + } + /** * AJAX handler for checking room availability. * diff --git a/src/PostTypes/Booking.php b/src/PostTypes/Booking.php index 8915a25..f027d1c 100644 --- a/src/PostTypes/Booking.php +++ b/src/PostTypes/Booking.php @@ -54,8 +54,24 @@ final class Booking { add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) ); add_action( 'pre_get_posts', array( self::class, 'filter_query' ) ); add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 ); - add_filter( 'wp_insert_post_data', array( self::class, 'auto_generate_title' ), 10, 2 ); add_action( 'admin_notices', array( self::class, 'show_conflict_notice' ) ); + + // Disable Gutenberg block editor for Bookings - use classic editor for form-based UI. + add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 ); + } + + /** + * Disable block editor for Bookings post type. + * + * @param bool $use_block_editor Whether to use block editor. + * @param string $post_type Post type. + * @return bool + */ + public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool { + if ( self::POST_TYPE === $post_type ) { + return false; + } + return $use_block_editor; } /** @@ -296,7 +312,7 @@ final class Booking { * @return void */ public static function render_guest_meta_box( \WP_Post $post ): void { - $guest_id = get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true ); + $guest_id = (int) get_post_meta( $post->ID, self::META_PREFIX . 'guest_id', true ); $guest_name = get_post_meta( $post->ID, self::META_PREFIX . 'guest_name', true ); $guest_email = get_post_meta( $post->ID, self::META_PREFIX . 'guest_email', true ); $guest_phone = get_post_meta( $post->ID, self::META_PREFIX . 'guest_phone', true ); @@ -314,7 +330,7 @@ final class Booking { $guest_phone = get_post_meta( $guest_id, '_bnb_guest_phone', true ); } else { $linked_guest = null; - $guest_id = ''; + $guest_id = 0; } } ?> @@ -757,23 +773,26 @@ final class Booking { delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' ); } } else { - delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' ); + // No guest_id selected - get guest data from form fields. + $guest_name = isset( $_POST['bnb_booking_guest_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_name'] ) ) : ''; + $guest_email = isset( $_POST['bnb_booking_guest_email'] ) ? sanitize_email( wp_unslash( $_POST['bnb_booking_guest_email'] ) ) : ''; + $guest_phone = isset( $_POST['bnb_booking_guest_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_booking_guest_phone'] ) ) : ''; - // Guest text fields (only save if no guest_id). - $guest_fields = array( 'guest_name', 'guest_email', 'guest_phone', 'guest_notes' ); - foreach ( $guest_fields as $field ) { - $key = 'bnb_booking_' . $field; - if ( isset( $_POST[ $key ] ) ) { - $value = wp_unslash( $_POST[ $key ] ); - if ( 'guest_email' === $field ) { - $value = sanitize_email( $value ); - } elseif ( 'guest_notes' === $field ) { - $value = sanitize_textarea_field( $value ); - } else { - $value = sanitize_text_field( $value ); - } - update_post_meta( $post_id, self::META_PREFIX . $field, $value ); - } + // Try to find or create a Guest post. + $linked_guest_id = self::find_or_create_guest( $guest_name, $guest_email, $guest_phone ); + + if ( $linked_guest_id ) { + // Link to the guest and sync data. + update_post_meta( $post_id, self::META_PREFIX . 'guest_id', $linked_guest_id ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_name', Guest::get_full_name( $linked_guest_id ) ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_email', get_post_meta( $linked_guest_id, '_bnb_guest_email', true ) ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', get_post_meta( $linked_guest_id, '_bnb_guest_phone', true ) ); + } else { + // Fallback: save guest data directly to booking meta if guest creation failed. + delete_post_meta( $post_id, self::META_PREFIX . 'guest_id' ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_name', $guest_name ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_email', $guest_email ); + update_post_meta( $post_id, self::META_PREFIX . 'guest_phone', $guest_phone ); } } @@ -877,6 +896,130 @@ final class Booking { */ do_action( 'wp_bnb_booking_status_changed', $post_id, $old_status, $status ); } + + // Generate comprehensive title with guest name, room, and dates. + self::generate_comprehensive_title( $post_id, $room_id, $check_in, $check_out ); + } + + /** + * Generate a comprehensive title for a booking. + * + * Format: "Guest Name (DD.MM - DD.MM.YYYY)" + * + * @param int $post_id Booking post ID. + * @param int $room_id Room post ID (unused, kept for signature compatibility). + * @param string $check_in Check-in date (Y-m-d). + * @param string $check_out Check-out date (Y-m-d). + * @return void + */ + private static function generate_comprehensive_title( int $post_id, int $room_id, string $check_in, string $check_out ): void { + // Get guest name. + $guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true ); + if ( empty( $guest_name ) ) { + $guest_name = __( 'Unknown Guest', 'wp-bnb' ); + } + + // Format dates. + $date_part = ''; + if ( $check_in && $check_out ) { + $check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in ); + $check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out ); + + if ( $check_in_date && $check_out_date ) { + // Same year: "01.02 - 05.02.2026" + // Different year: "28.12.2025 - 02.01.2026" + if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) { + $date_part = sprintf( + '%s - %s', + $check_in_date->format( 'd.m' ), + $check_out_date->format( 'd.m.Y' ) + ); + } else { + $date_part = sprintf( + '%s - %s', + $check_in_date->format( 'd.m.Y' ), + $check_out_date->format( 'd.m.Y' ) + ); + } + } + } + + // Build title: "Guest Name (dates)". + $title = $guest_name; + if ( $date_part ) { + $title .= sprintf( ' (%s)', $date_part ); + } + + // Update the post title directly in the database to avoid infinite loop. + global $wpdb; + $wpdb->update( + $wpdb->posts, + array( 'post_title' => $title ), + array( 'ID' => $post_id ), + array( '%s' ), + array( '%d' ) + ); + + // Clear post cache. + clean_post_cache( $post_id ); + } + + /** + * Find an existing guest by email or create a new one. + * + * @param string $name Guest full name. + * @param string $email Guest email. + * @param string $phone Guest phone (optional). + * @return int|null Guest post ID or null on failure. + */ + private static function find_or_create_guest( string $name, string $email, string $phone = '' ): ?int { + // Need at least a name to create a guest. + if ( empty( $name ) ) { + return null; + } + + // Try to find existing guest by email. + if ( ! empty( $email ) ) { + $existing_guest = Guest::get_by_email( $email ); + if ( $existing_guest ) { + return $existing_guest->ID; + } + } + + // Parse name into first/last name. + $name_parts = explode( ' ', trim( $name ), 2 ); + $first_name = $name_parts[0] ?? ''; + $last_name = $name_parts[1] ?? ''; + + // Create new guest post. + $guest_id = wp_insert_post( + array( + 'post_type' => Guest::POST_TYPE, + 'post_status' => 'publish', + 'post_title' => $name, + ) + ); + + if ( is_wp_error( $guest_id ) || ! $guest_id ) { + return null; + } + + // Save guest meta. + update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name ); + update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name ); + + if ( ! empty( $email ) ) { + update_post_meta( $guest_id, '_bnb_guest_email', $email ); + } + + if ( ! empty( $phone ) ) { + update_post_meta( $guest_id, '_bnb_guest_phone', $phone ); + } + + // Set default status. + update_post_meta( $guest_id, '_bnb_guest_status', 'active' ); + + return $guest_id; } /** @@ -913,7 +1056,7 @@ final class Booking { public static function render_column( string $column, int $post_id ): void { switch ( $column ) { case 'room': - $room_id = get_post_meta( $post_id, self::META_PREFIX . 'room_id', true ); + $room_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'room_id', true ); if ( $room_id ) { $room = get_post( $room_id ); if ( $room ) { @@ -935,7 +1078,7 @@ final class Booking { break; case 'guest': - $guest_id = get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true ); + $guest_id = (int) get_post_meta( $post_id, self::META_PREFIX . 'guest_id', true ); $guest_name = get_post_meta( $post_id, self::META_PREFIX . 'guest_name', true ); $guest_email = get_post_meta( $post_id, self::META_PREFIX . 'guest_email', true ); if ( $guest_name ) { @@ -1096,6 +1239,13 @@ final class Booking { return; } + // Exclude auto-drafts from the list - they're not real bookings. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. + $post_status = isset( $_GET['post_status'] ) ? sanitize_key( $_GET['post_status'] ) : ''; + if ( empty( $post_status ) || 'all' === $post_status ) { + $query->set( 'post_status', array( 'publish', 'pending', 'draft', 'private' ) ); + } + $meta_query = array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filter query only. @@ -1142,31 +1292,11 @@ final class Booking { */ public static function change_title_placeholder( string $placeholder, \WP_Post $post ): string { if ( self::POST_TYPE === $post->post_type ) { - return __( 'Booking reference (auto-generated)', 'wp-bnb' ); + return __( 'Title auto-generated from guest name and dates', 'wp-bnb' ); } return $placeholder; } - /** - * Auto-generate booking reference as title. - * - * @param array $data Post data. - * @param array $postarr Post array. - * @return array - */ - public static function auto_generate_title( array $data, array $postarr ): array { - if ( self::POST_TYPE !== $data['post_type'] ) { - return $data; - } - - // Only generate if title is empty or matches auto-generated pattern. - if ( empty( $data['post_title'] ) || preg_match( '/^BNB-\d{4}-\d{5}$/', $data['post_title'] ) ) { - $data['post_title'] = self::generate_reference(); - } - - return $data; - } - /** * Show conflict notice in admin. * diff --git a/src/PostTypes/Guest.php b/src/PostTypes/Guest.php index 37b70e0..82ce41f 100644 --- a/src/PostTypes/Guest.php +++ b/src/PostTypes/Guest.php @@ -30,6 +30,75 @@ final class Guest { */ private const META_PREFIX = '_bnb_guest_'; + /** + * Encryption method. + * + * @var string + */ + private const ENCRYPTION_METHOD = 'aes-256-cbc'; + + /** + * Encrypt sensitive data. + * + * Uses WordPress AUTH_KEY for encryption key derivation. + * + * @param string $data Plain text data to encrypt. + * @return string Encrypted data (base64 encoded). + */ + private static function encrypt( string $data ): string { + if ( empty( $data ) ) { + return ''; + } + + $key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true ); + $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::ENCRYPTION_METHOD ) ); + + $encrypted = openssl_encrypt( $data, self::ENCRYPTION_METHOD, $key, 0, $iv ); + + if ( false === $encrypted ) { + return ''; + } + + // Store IV with encrypted data (IV is not secret, just needs to be unique). + return base64_encode( $iv . '::' . $encrypted ); + } + + /** + * Decrypt sensitive data. + * + * @param string $data Encrypted data (base64 encoded). + * @return string Decrypted plain text. + */ + private static function decrypt( string $data ): string { + if ( empty( $data ) ) { + return ''; + } + + $decoded = base64_decode( $data, true ); + if ( false === $decoded ) { + // Data might be stored unencrypted (legacy), return as-is. + return $data; + } + + $parts = explode( '::', $decoded, 2 ); + if ( count( $parts ) !== 2 ) { + // Data might be stored unencrypted (legacy), return as-is. + return $data; + } + + list( $iv, $encrypted ) = $parts; + $key = hash( 'sha256', AUTH_KEY . 'wp_bnb_guest_encryption', true ); + + $decrypted = openssl_decrypt( $encrypted, self::ENCRYPTION_METHOD, $key, 0, $iv ); + + if ( false === $decrypted ) { + // Decryption failed, might be legacy unencrypted data. + return $data; + } + + return $decrypted; + } + /** * Initialize the post type. * @@ -45,6 +114,23 @@ final class Guest { add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) ); add_action( 'pre_get_posts', array( self::class, 'filter_query' ) ); add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 ); + + // Disable Gutenberg block editor for Guests - use classic editor for simpler UI. + add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 ); + } + + /** + * Disable block editor for Guests post type. + * + * @param bool $use_block_editor Whether to use block editor. + * @param string $post_type Post type. + * @return bool + */ + public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool { + if ( self::POST_TYPE === $post_type ) { + return false; + } + return $use_block_editor; } /** @@ -311,12 +397,12 @@ final class Guest { */ public static function render_identification_meta_box( \WP_Post $post ): void { $id_type = get_post_meta( $post->ID, self::META_PREFIX . 'id_type', true ); - $id_number = get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ); + $id_number = self::decrypt( get_post_meta( $post->ID, self::META_PREFIX . 'id_number', true ) ); $id_expiry = get_post_meta( $post->ID, self::META_PREFIX . 'id_expiry', true ); ?>

- - + +

@@ -443,9 +529,9 @@ final class Guest { $room = get_post( $room_id ); $check_in = get_post_meta( $booking->ID, '_bnb_booking_check_in', true ); $check_out = get_post_meta( $booking->ID, '_bnb_booking_check_out', true ); - $price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); + $price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true ); $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); - $statuses = Booking::get_statuses(); + $statuses = Booking::get_booking_statuses(); $colors = Booking::get_status_colors(); ?> @@ -563,7 +649,7 @@ final class Guest { return; } - // Text fields. + // Text fields (non-sensitive). $text_fields = array( 'first_name', 'last_name', @@ -574,7 +660,6 @@ final class Guest { 'country', 'nationality', 'id_type', - 'id_number', 'status', ); @@ -589,6 +674,16 @@ final class Guest { } } + // Sensitive field: ID number (encrypted). + if ( isset( $_POST['bnb_guest_id_number'] ) ) { + $id_number = sanitize_text_field( wp_unslash( $_POST['bnb_guest_id_number'] ) ); + update_post_meta( + $post_id, + self::META_PREFIX . 'id_number', + self::encrypt( $id_number ) + ); + } + // Email field (special sanitization). if ( isset( $_POST['bnb_guest_email'] ) ) { update_post_meta( @@ -1035,7 +1130,7 @@ final class Guest { $status = get_post_meta( $booking->ID, '_bnb_booking_status', true ); // Only count completed bookings (checked_out) or confirmed ones. if ( in_array( $status, array( 'confirmed', 'checked_in', 'checked_out' ), true ) ) { - $price = get_post_meta( $booking->ID, '_bnb_booking_total_price', true ); + $price = get_post_meta( $booking->ID, '_bnb_booking_calculated_price', true ); $total += floatval( $price ); } } @@ -1083,4 +1178,15 @@ final class Guest { return trim( $first_name . ' ' . $last_name ); } + + /** + * Get guest's ID number (decrypted). + * + * @param int $guest_id Guest post ID. + * @return string Decrypted ID number. + */ + public static function get_id_number( int $guest_id ): string { + $encrypted = get_post_meta( $guest_id, self::META_PREFIX . 'id_number', true ); + return self::decrypt( $encrypted ); + } } diff --git a/src/PostTypes/Service.php b/src/PostTypes/Service.php index aa83bb2..01ae153 100644 --- a/src/PostTypes/Service.php +++ b/src/PostTypes/Service.php @@ -47,6 +47,23 @@ final class Service { add_action( 'restrict_manage_posts', array( self::class, 'add_filters' ) ); add_action( 'pre_get_posts', array( self::class, 'filter_query' ) ); add_filter( 'enter_title_here', array( self::class, 'change_title_placeholder' ), 10, 2 ); + + // Disable Gutenberg block editor for Services - use classic editor for simpler UI. + add_filter( 'use_block_editor_for_post_type', array( self::class, 'disable_block_editor' ), 10, 2 ); + } + + /** + * Disable block editor for Services post type. + * + * @param bool $use_block_editor Whether to use block editor. + * @param string $post_type Post type. + * @return bool + */ + public static function disable_block_editor( bool $use_block_editor, string $post_type ): bool { + if ( self::POST_TYPE === $post_type ) { + return false; + } + return $use_block_editor; } /** diff --git a/wp-bnb.php b/wp-bnb.php index b2e1641..3b391e9 100644 --- a/wp-bnb.php +++ b/wp-bnb.php @@ -3,7 +3,7 @@ * Plugin Name: WP BnB Management * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb * Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests. - * Version: 0.6.0 + * Version: 0.6.1 * Requires at least: 6.0 * Requires PHP: 8.3 * Author: Marco Graetsch @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Plugin version constant - MUST match Version in header above. -define( 'WP_BNB_VERSION', '0.6.0' ); +define( 'WP_BNB_VERSION', '0.6.1' ); // Plugin path constants. define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );