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 <noreply@anthropic.com>
This commit is contained in:
69
CHANGELOG.md
69
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
|
||||
|
||||
65
README.md
65
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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
'<span style="color: #00a32a; font-weight: 600;">' +
|
||||
data.latest_version +
|
||||
'</span> ' +
|
||||
'<span class="dashicons dashicons-yes" style="color: #00a32a;"></span> ' +
|
||||
'<em>' + (wpBnbAdmin.i18n.updateAvailable || 'Update available!') + '</em>'
|
||||
);
|
||||
showUpdateMessage('success', data.message);
|
||||
} else {
|
||||
$latestVersion.html(
|
||||
data.latest_version +
|
||||
' <span style="color: #646970;">' +
|
||||
(wpBnbAdmin.i18n.upToDate || '(You are up to date)') +
|
||||
'</span>'
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
473
src/License/Updater.php
Normal file
473
src/License/Updater.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
712
src/Plugin.php
712
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();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'WP BnB Dashboard', 'wp-bnb' ); ?></h1>
|
||||
|
||||
<?php if ( 'valid' !== $license_status ) : ?>
|
||||
<?php if ( $is_localhost ) : ?>
|
||||
<div class="notice notice-info">
|
||||
<p>
|
||||
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||
<?php esc_html_e( 'You are running on a local development environment. All features are enabled.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php elseif ( ! $license_valid ) : ?>
|
||||
<div class="notice notice-warning">
|
||||
<p>
|
||||
<?php
|
||||
@@ -464,6 +549,10 @@ final class Plugin {
|
||||
class="nav-tab <?php echo 'license' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'License', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-settings&tab=updates' ) ); ?>"
|
||||
class="nav-tab <?php echo 'updates' === $active_tab ? 'nav-tab-active' : ''; ?>">
|
||||
<?php esc_html_e( 'Updates', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -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 {
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
|
||||
<h2><?php esc_html_e( 'Business Information', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
@@ -532,6 +626,143 @@ final class Plugin {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_address_street"><?php esc_html_e( 'Street Address', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="wp_bnb_address_street" id="wp_bnb_address_street"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_address_street', '' ) ); ?>"
|
||||
class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_address_city"><?php esc_html_e( 'City', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="wp_bnb_address_city" id="wp_bnb_address_city"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_address_city', '' ) ); ?>"
|
||||
class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_address_postal"><?php esc_html_e( 'Postal Code', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="wp_bnb_address_postal" id="wp_bnb_address_postal"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_address_postal', '' ) ); ?>"
|
||||
class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_address_country"><?php esc_html_e( 'Country', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="wp_bnb_address_country" id="wp_bnb_address_country"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_address_country', '' ) ); ?>"
|
||||
class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Contact Information', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_contact_email"><?php esc_html_e( 'Email Address', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="email" name="wp_bnb_contact_email" id="wp_bnb_contact_email"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_email', '' ) ); ?>"
|
||||
class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Primary contact email for bookings and inquiries.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_contact_phone"><?php esc_html_e( 'Phone Number', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="tel" name="wp_bnb_contact_phone" id="wp_bnb_contact_phone"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_phone', '' ) ); ?>"
|
||||
class="regular-text" placeholder="+41 12 345 67 89">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_contact_website"><?php esc_html_e( 'Website', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_contact_website" id="wp_bnb_contact_website"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_contact_website', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Social Media', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_social_facebook"><?php esc_html_e( 'Facebook', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_social_facebook" id="wp_bnb_social_facebook"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_social_facebook', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://facebook.com/yourpage">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_social_instagram"><?php esc_html_e( 'Instagram', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_social_instagram" id="wp_bnb_social_instagram"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_social_instagram', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://instagram.com/yourprofile">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_social_x"><?php esc_html_e( 'X (Twitter)', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_social_x" id="wp_bnb_social_x"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_social_x', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://x.com/yourhandle">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_social_linkedin"><?php esc_html_e( 'LinkedIn', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_social_linkedin" id="wp_bnb_social_linkedin"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_social_linkedin', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://linkedin.com/company/yourcompany">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_social_tripadvisor"><?php esc_html_e( 'TripAdvisor', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="wp_bnb_social_tripadvisor" id="wp_bnb_social_tripadvisor"
|
||||
value="<?php echo esc_attr( get_option( 'wp_bnb_social_tripadvisor', '' ) ); ?>"
|
||||
class="regular-text" placeholder="https://tripadvisor.com/...">
|
||||
<p class="description"><?php esc_html_e( 'Link to your TripAdvisor listing.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button( __( 'Save Settings', 'wp-bnb' ) ); ?>
|
||||
</form>
|
||||
<?php
|
||||
@@ -543,12 +774,14 @@ final class Plugin {
|
||||
* @return void
|
||||
*/
|
||||
private function render_pricing_settings(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Subtab switching only.
|
||||
$active_subtab = isset( $_GET['subtab'] ) ? sanitize_key( $_GET['subtab'] ) : 'tiers';
|
||||
$short_term_max = get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||
$mid_term_max = get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||
$weekend_days = get_option( 'wp_bnb_weekend_days', '5,6' );
|
||||
$seasons = Season::all();
|
||||
|
||||
$days_of_week = array(
|
||||
$days_of_week = array(
|
||||
1 => __( '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' );
|
||||
?>
|
||||
|
||||
<!-- Pricing Subtabs -->
|
||||
<div class="wp-bnb-subtabs">
|
||||
<a href="<?php echo esc_url( $base_url . '&subtab=tiers' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'tiers' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-chart-bar"></span>
|
||||
<?php esc_html_e( 'Pricing Tiers', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $base_url . '&subtab=weekend' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'weekend' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-calendar-alt"></span>
|
||||
<?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( $base_url . '&subtab=seasons' ); ?>"
|
||||
class="wp-bnb-subtab <?php echo 'seasons' === $active_subtab ? 'active' : ''; ?>">
|
||||
<span class="dashicons dashicons-image-filter"></span>
|
||||
<?php esc_html_e( 'Seasons', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
|
||||
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
||||
<?php if ( 'tiers' === $active_subtab ) : ?>
|
||||
<!-- Pricing Tiers Subtab -->
|
||||
<h2><?php esc_html_e( 'Pricing Tier Thresholds', 'wp-bnb' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Define the number of nights that determine which pricing tier applies.', 'wp-bnb' ); ?></p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_short_term_max_nights"><?php esc_html_e( 'Short-term (Nightly)', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_short_term_max_nights" id="wp_bnb_short_term_max_nights"
|
||||
value="<?php echo esc_attr( $short_term_max ); ?>"
|
||||
class="small-text" min="1" max="30">
|
||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Stays up to this many nights use the nightly rate.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_mid_term_max_nights"><?php esc_html_e( 'Mid-term (Weekly)', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_mid_term_max_nights" id="wp_bnb_mid_term_max_nights"
|
||||
value="<?php echo esc_attr( $mid_term_max ); ?>"
|
||||
class="small-text" min="7" max="90">
|
||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Stays longer than short-term but up to this many nights use the weekly rate.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Long-term (Monthly)', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: number of nights */
|
||||
esc_html__( 'Stays longer than %s nights use the monthly rate.', 'wp-bnb' ),
|
||||
'<strong id="wp-bnb-long-term-min">' . esc_html( $mid_term_max ) . '</strong>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Select which days are considered weekend days for weekend surcharges.', 'wp-bnb' ); ?></p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<?php foreach ( $days_of_week as $day_num => $day_name ) : ?>
|
||||
<label style="display: inline-block; margin-right: 15px;">
|
||||
<input type="checkbox" name="wp_bnb_weekend_days[]" value="<?php echo esc_attr( $day_num ); ?>"
|
||||
<?php checked( in_array( $day_num, $selected_days, true ) ); ?>>
|
||||
<?php echo esc_html( $day_name ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e( 'Weekend surcharges (configured per room) apply to nights starting on these days.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Link to seasons page */
|
||||
esc_html__( 'Manage seasonal pricing periods in the %s.', 'wp-bnb' ),
|
||||
'<a href="' . esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ) . '">' . esc_html__( 'Seasons Manager', 'wp-bnb' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php if ( ! empty( $seasons ) ) : ?>
|
||||
<table class="widefat striped" style="max-width: 600px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Season', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Modifier', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $seasons as $season ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $season->name ); ?></td>
|
||||
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
||||
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
||||
<td>
|
||||
<?php if ( $season->active ) : ?>
|
||||
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
|
||||
<?php else : ?>
|
||||
<span class="dashicons dashicons-marker" style="color: #646970;"></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_short_term_max_nights"><?php esc_html_e( 'Short-term (Nightly)', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_short_term_max_nights" id="wp_bnb_short_term_max_nights"
|
||||
value="<?php echo esc_attr( $short_term_max ); ?>"
|
||||
class="small-text" min="1" max="30">
|
||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Stays up to this many nights use the nightly rate.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_mid_term_max_nights"><?php esc_html_e( 'Mid-term (Weekly)', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_mid_term_max_nights" id="wp_bnb_mid_term_max_nights"
|
||||
value="<?php echo esc_attr( $mid_term_max ); ?>"
|
||||
class="small-text" min="7" max="90">
|
||||
<?php esc_html_e( 'nights or fewer', 'wp-bnb' ); ?>
|
||||
<p class="description"><?php esc_html_e( 'Stays longer than short-term but up to this many nights use the weekly rate.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Long-term (Monthly)', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: number of nights */
|
||||
esc_html__( 'Stays longer than %s nights use the monthly rate.', 'wp-bnb' ),
|
||||
'<strong id="wp-bnb-long-term-min">' . esc_html( $mid_term_max ) . '</strong>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e( 'No seasons configured yet.', 'wp-bnb' ); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php submit_button( __( 'Save Pricing Settings', 'wp-bnb' ) ); ?>
|
||||
<?php submit_button( __( 'Save Pricing Tiers', 'wp-bnb' ) ); ?>
|
||||
|
||||
<?php elseif ( 'weekend' === $active_subtab ) : ?>
|
||||
<!-- Weekend Days Subtab -->
|
||||
<h2><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Select which days are considered weekend days for weekend surcharges.', 'wp-bnb' ); ?></p>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Weekend Days', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<?php foreach ( $days_of_week as $day_num => $day_name ) : ?>
|
||||
<label style="display: inline-block; margin-right: 15px;">
|
||||
<input type="checkbox" name="wp_bnb_weekend_days[]" value="<?php echo esc_attr( $day_num ); ?>"
|
||||
<?php checked( in_array( $day_num, $selected_days, true ) ); ?>>
|
||||
<?php echo esc_html( $day_name ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e( 'Weekend surcharges (configured per room) apply to nights starting on these days.', 'wp-bnb' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button( __( 'Save Weekend Days', 'wp-bnb' ) ); ?>
|
||||
|
||||
<?php else : ?>
|
||||
<!-- Seasons Subtab -->
|
||||
<h2><?php esc_html_e( 'Seasonal Pricing', 'wp-bnb' ); ?></h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Seasonal pricing allows you to adjust room rates based on time of year. Prices are multiplied by the season modifier.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wp-bnb-seasons' ) ); ?>" class="button button-primary">
|
||||
<span class="dashicons dashicons-admin-settings" style="vertical-align: text-top;"></span>
|
||||
<?php esc_html_e( 'Manage Seasons', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<?php if ( ! empty( $seasons ) ) : ?>
|
||||
<table class="widefat striped" style="max-width: 700px; margin-top: 20px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Season', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Period', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Modifier', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Priority', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Status', 'wp-bnb' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $seasons as $season ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( $season->name ); ?></strong></td>
|
||||
<td><?php echo esc_html( $season->start_date . ' - ' . $season->end_date ); ?></td>
|
||||
<td><?php echo esc_html( $season->getModifierLabel() ); ?></td>
|
||||
<td><?php echo esc_html( $season->priority ); ?></td>
|
||||
<td>
|
||||
<?php if ( $season->active ) : ?>
|
||||
<span class="bnb-status-active"><?php esc_html_e( 'Active', 'wp-bnb' ); ?></span>
|
||||
<?php else : ?>
|
||||
<span class="bnb-status-inactive"><?php esc_html_e( 'Inactive', 'wp-bnb' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<div class="notice notice-info inline" style="margin: 20px 0;">
|
||||
<p><?php esc_html_e( 'No seasons configured yet. Create seasons to apply price modifiers during specific periods.', 'wp-bnb' ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
@@ -685,10 +955,21 @@ final class Plugin {
|
||||
$license_status = LicenseManager::get_cached_status();
|
||||
$license_data = LicenseManager::get_cached_data();
|
||||
$last_check = LicenseManager::get_last_check();
|
||||
$is_localhost = LicenseManager::is_localhost();
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
|
||||
<?php if ( $is_localhost ) : ?>
|
||||
<div class="notice notice-info inline" style="margin: 0 0 20px 0;">
|
||||
<p>
|
||||
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
|
||||
<strong><?php esc_html_e( 'Development Mode', 'wp-bnb' ); ?></strong>
|
||||
<?php esc_html_e( 'You are running on a local development environment. License validation is bypassed and all features are enabled.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?php esc_html_e( 'License Status', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
@@ -773,6 +1054,160 @@ final class Plugin {
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render updates settings tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_updates_settings(): void {
|
||||
$updater = LicenseUpdater::get_instance();
|
||||
|
||||
if ( null === $updater ) {
|
||||
?>
|
||||
<p><?php esc_html_e( 'Update checker is not available.', 'wp-bnb' ); ?></p>
|
||||
<?php
|
||||
return;
|
||||
}
|
||||
|
||||
$current_version = $updater->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, '<' );
|
||||
}
|
||||
?>
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'wp_bnb_save_settings', 'wp_bnb_settings_nonce' ); ?>
|
||||
|
||||
<h2><?php esc_html_e( 'Update Status', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Current Version', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<strong><?php echo esc_html( $current_version ); ?></strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Latest Version', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<span id="wp-bnb-latest-version">
|
||||
<?php if ( $update_available ) : ?>
|
||||
<span style="color: #00a32a; font-weight: 600;">
|
||||
<?php echo esc_html( $latest_version ); ?>
|
||||
</span>
|
||||
<span class="dashicons dashicons-yes" style="color: #00a32a;"></span>
|
||||
<em><?php esc_html_e( 'Update available!', 'wp-bnb' ); ?></em>
|
||||
<?php else : ?>
|
||||
<?php echo esc_html( $latest_version ); ?>
|
||||
<span style="color: #646970;">
|
||||
<?php esc_html_e( '(You are up to date)', 'wp-bnb' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Last Check', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<span id="wp-bnb-update-last-check">
|
||||
<?php if ( $last_check > 0 ) : ?>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Time ago string */
|
||||
esc_html__( '%s ago', 'wp-bnb' ),
|
||||
esc_html( human_time_diff( $last_check, time() ) )
|
||||
);
|
||||
?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( 'Never', 'wp-bnb' ); ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-bottom: 30px;">
|
||||
<button type="button" id="wp-bnb-check-updates" class="button button-secondary">
|
||||
<span class="dashicons dashicons-update" style="vertical-align: text-top;"></span>
|
||||
<?php esc_html_e( 'Check for Updates', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
<span class="spinner" id="wp-bnb-update-spinner"></span>
|
||||
<?php if ( $update_available ) : ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'plugins.php' ) ); ?>" class="button button-primary">
|
||||
<?php esc_html_e( 'Go to Plugins Page', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<div id="wp-bnb-update-message" style="display: none; margin-bottom: 20px;"></div>
|
||||
|
||||
<h2><?php esc_html_e( 'Update Settings', 'wp-bnb' ); ?></h2>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Enable Update Notifications', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wp_bnb_update_notifications_enabled"
|
||||
value="yes" <?php checked( $notifications_enabled ); ?>>
|
||||
<?php esc_html_e( 'Check for updates from the license server', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'When enabled, the plugin will check for updates and show notifications in the WordPress admin.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Automatic Updates', 'wp-bnb' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="wp_bnb_auto_install_enabled"
|
||||
value="yes" <?php checked( $auto_install_enabled ); ?>
|
||||
<?php disabled( ! $license_valid ); ?>>
|
||||
<?php esc_html_e( 'Automatically install updates', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<?php if ( ! $license_valid ) : ?>
|
||||
<span style="color: #d63638; margin-left: 10px;">
|
||||
<?php esc_html_e( '(Requires valid license)', 'wp-bnb' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'When enabled, updates will be automatically installed during WordPress background updates.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wp_bnb_update_check_frequency"><?php esc_html_e( 'Check Frequency', 'wp-bnb' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wp_bnb_update_check_frequency" id="wp_bnb_update_check_frequency"
|
||||
value="<?php echo esc_attr( $check_frequency ); ?>"
|
||||
min="1" max="168" class="small-text">
|
||||
<?php esc_html_e( 'hours', 'wp-bnb' ); ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'How often to check for updates (1-168 hours). Default: 12 hours.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<?php submit_button( __( 'Save Update Settings', 'wp-bnb' ), 'primary', 'submit', false ); ?>
|
||||
</p>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render license status badge.
|
||||
*
|
||||
@@ -841,6 +1276,9 @@ final class Plugin {
|
||||
case 'license':
|
||||
$this->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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 );
|
||||
?>
|
||||
<p class="description" style="margin-bottom: 15px;">
|
||||
<span class="dashicons dashicons-shield" style="color: #d63638;"></span>
|
||||
<?php esc_html_e( 'This information is sensitive. Handle with care according to privacy regulations.', 'wp-bnb' ); ?>
|
||||
<span class="dashicons dashicons-shield" style="color: #00a32a;"></span>
|
||||
<?php esc_html_e( 'This information is encrypted and stored securely.', 'wp-bnb' ); ?>
|
||||
</p>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
@@ -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();
|
||||
?>
|
||||
<tr>
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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__ ) );
|
||||
|
||||
Reference in New Issue
Block a user