9 Commits

Author SHA1 Message Date
a784d92cc9 Add CF7 tag generator buttons for admin form editor (v0.7.1)
All checks were successful
Create Release Package / build-release (push) Successful in 59s
- Register tag generators via wpcf7_admin_init hook
- Add BnB Building select tag generator with first_as_label option
- Add BnB Room select tag generator with building_field and include_price options
- Add BnB Check-in date tag generator with min/max advance options
- Add BnB Check-out date tag generator with checkin_field and min/max nights options
- Add BnB Guests count tag generator with room_field and min/max/default options
- All generators support id and class attribute configuration
- Remove bug from Known Bugs section in CLAUDE.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:00:26 +01:00
f61dca5f45 Update CLAUDE.md with v0.7.0 session history
Document CF7 integration implementation details:
- Directory structure updated with Integration folder
- New CF7 assets (JS, CSS) documented
- Session history with learnings and patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:34:00 +01:00
28350aabfa Implement Phase 7: Contact Form 7 Integration (v0.7.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m1s
Add custom CF7 form tags for booking requests:
- [bnb_building_select] - Building filter dropdown
- [bnb_room_select] - Room selection with capacity data
- [bnb_date_checkin/checkout] - Date pickers with validation
- [bnb_guests] - Guest count with capacity limits

Features:
- Server-side validation for all fields
- Real-time AJAX availability checking
- Automatic price calculation display
- Booking creation on form submission
- Guest record creation/linking
- Custom mail tags for CF7 templates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:30:30 +01:00
3579904bad Add Phase 10: Security Audit to roadmap
- Added security audit phase (v0.10.0) to PLAN.md
- WordPress best practices review
- OWASP Top 10 review (XSS, XSRF, SQLi, etc.)
- Updated version milestones table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:10:19 +01:00
602549208f Fix MD012 linting warning in CLAUDE.md
Remove duplicate blank line between session history sections.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:03:40 +01:00
45a73e15aa Add Phase 9: Prometheus Metrics to roadmap
- Added Prometheus metrics integration phase (v0.9.0) to PLAN.md
- Includes meaningful metrics for plugin, example Grafana dashboard
- Settings page option to enable/disable metrics
- Links to wp-prometheus README for implementation details
- Updated version milestones table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 16:02:17 +01:00
13ba264431 Release v0.6.1 - Bug fixes and enhancements
All checks were successful
Create Release Package / build-release (push) Successful in 1m1s
New Features:
- Auto-update system with configurable check frequency
- Updates tab in settings with manual check button
- Localhost development mode bypasses license validation
- Extended general settings (address, contact, social media)
- Pricing settings split into subtabs
- Guest ID/passport encryption using AES-256-CBC
- Guest auto-creation from booking form

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:18:27 +01:00
c17dd53c5a Update CLAUDE.md with session history and learnings
- Document all bug fixes and enhancements from 2026-02-03 session
- Add learnings about Gutenberg vs classic editor for form-based post types
- Document encryption implementation for guest data
- Add notes on auto-draft handling and type safety

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:15:16 +01:00
be2735a3bd Update CLAUDE.md with v0.6.0 session history and directory structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:12:22 +01:00
16 changed files with 4605 additions and 197 deletions

View File

@@ -5,6 +5,132 @@ 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.7.1] - 2026-02-03
### Added
- CF7 Admin Tag Generator buttons:
- Tag generator buttons appear in CF7 form editor for all WP BnB custom tags
- BnB Building select with first option label configuration
- BnB Room select with building field linking and price display options
- BnB Check-in date with min/max advance booking days
- BnB Check-out date with check-in field linking and min/max nights
- BnB Guests count with room field linking and min/max/default values
- All generators support id and class attribute configuration
## [0.7.0] - 2026-02-03
### Added
- Contact Form 7 Integration:
- New `src/Integration/CF7.php` class for CF7 integration
- Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
- Server-side validation for all custom tags
- Availability checking before form submission
- Automatic booking creation on form submission with 'pending' status
- Guest record creation/linking using existing `find_or_create_guest` pattern
- Price calculation using existing Calculator class
- Email notifications via existing EmailNotifier
- CF7 Frontend Assets:
- `assets/js/cf7-integration.js` for dynamic form behavior
- Building-based room filtering
- Date linking (checkout min = checkin + 1)
- Capacity validation against selected room
- AJAX availability checking with status display
- Dynamic price calculation display
- `assets/css/cf7-integration.css` for form styling
- Availability status indicators (checking/available/unavailable)
- Price display formatting
- Capacity warning styling
- Responsive design with dark mode support
- Custom CF7 Mail Tags:
- `[_bnb_booking_reference]` - Generated booking reference
- `[_bnb_booking_id]` - Booking post ID
- `[_bnb_room_name]` - Selected room title
- `[_bnb_calculated_price]` - Formatted price
- `[_bnb_nights]` - Number of nights
- Form Type Detection:
- Auto-detects booking forms by presence of `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`
- CSS class `wp-bnb-booking-form` for explicit form type declaration
- Inquiry forms use default CF7 email handling without booking creation
### Changed
- Plugin.php updated to conditionally initialize CF7 integration when CF7 is active
- Frontend assets now include CF7-specific CSS and JavaScript when CF7 is detected
### Dependencies
- Contact Form 7 plugin required for CF7 integration features (optional)
## [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 +484,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

256
CLAUDE.md
View File

@@ -1,4 +1,4 @@
# WordPress BnB Management
# WordPress BnB Manager
**Author:** Marco Graetsch
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
@@ -38,6 +38,10 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
### Known Bugs
(none)
## Technical Stack
- **Language:** PHP 8.3.x
@@ -234,9 +238,18 @@ wp-bnb/
│ ├── Admin/ # Admin pages
│ │ ├── Calendar.php # Availability calendar page
│ │ └── Seasons.php # Seasons management page
│ ├── Blocks/ # Gutenberg blocks
│ │ └── BlockRegistrar.php # Block registration and rendering
│ ├── Booking/ # Booking system
│ │ ├── Availability.php # Availability checking
│ │ └── EmailNotifier.php # Email notifications
│ ├── Frontend/ # Frontend components
│ │ ├── Search.php # Room search and AJAX handlers
│ │ ├── Shortcodes.php # All shortcode handlers
│ │ └── Widgets/ # WordPress widgets
│ │ ├── AvailabilityCalendar.php
│ │ ├── BuildingRooms.php
│ │ └── SimilarRooms.php
│ ├── License/
│ │ └── Manager.php # License management
│ ├── PostTypes/ # Custom post types
@@ -247,6 +260,8 @@ wp-bnb/
│ │ ├── Calculator.php # Price calculation
│ │ ├── PricingTier.php # Pricing tier enum
│ │ └── Season.php # Seasonal pricing
│ ├── Integration/ # Third-party integrations
│ │ └── CF7.php # Contact Form 7 integration
│ └── Taxonomies/ # Custom taxonomies
│ ├── Amenity.php # Amenity taxonomy (tags)
│ └── RoomType.php # Room type taxonomy (categories)
@@ -256,10 +271,14 @@ wp-bnb/
├── assets/
│ ├── css/
│ │ ├── admin.css # Admin styles
│ │ ── frontend.css # Frontend styles
│ │ ── blocks-editor.css # Gutenberg editor styles
│ │ ├── cf7-integration.css # CF7 form styles
│ │ └── frontend.css # Frontend styles (~1250 lines)
│ └── js/
│ ├── admin.js # Admin scripts
── frontend.js # Frontend scripts
── blocks-editor.js # Gutenberg editor scripts
│ ├── cf7-integration.js # CF7 form scripts
│ └── frontend.js # Frontend scripts (~825 lines)
├── templates/ # Twig templates (future)
├── languages/ # Translation files (future)
└── releases/ # Release packages (git-ignored)
@@ -323,7 +342,7 @@ Admin features always work; frontend requires valid license.
- Implemented license settings page with validation/activation buttons
- Created admin CSS and JavaScript for license management
- Created Gitea CI/CD pipeline at `.gitea/workflows/release.yml`
- Created `PLAN.md` with full implementation roadmap (8 phases)
- Created `PLAN.md` with full implementation roadmap (10 phases)
- Created `README.md` with user documentation
- Created `CHANGELOG.md` following Keep a Changelog format
- Updated `CLAUDE.md` with architecture details
@@ -560,3 +579,232 @@ Admin features always work; frontend requires valid license.
- Same namespace classes can reference each other directly without use statements
- Services meta box renders before pricing meta box so services total is available
- Grand total calculation happens both on save (server-side) and on change (client-side JS)
### 2026-02-02 - Version 0.6.0 (Frontend Features)
**Completed:**
- Created `src/Frontend/Search.php` class
- Room search with multiple filters: availability, capacity, room type, amenities, price range, building
- AJAX endpoints: `wp_bnb_search_rooms`, `wp_bnb_get_availability`, `wp_bnb_get_calendar`, `wp_bnb_calculate_price`
- Pagination support with configurable per_page
- Room data formatting for JSON responses with thumbnails, pricing, amenities
- Price range filtering using Calculator integration
- Availability filtering using Availability class
- Created `src/Frontend/Shortcodes.php` class
- `[bnb_buildings]` - Buildings list/grid with layout, columns, limit, orderby options
- `[bnb_rooms]` - Rooms list/grid with building, room_type, amenities filters
- `[bnb_room_search]` - Interactive search form with results container
- `[bnb_building id="X"]` - Single building display with rooms
- `[bnb_room id="X"]` - Single room display with availability form
- Grid system with 1-4 column support
- Sorting options: title, date, price, capacity
- Created `src/Frontend/Widgets/` directory with three widgets
- `SimilarRooms.php` - Shows rooms from same building/room type
- `BuildingRooms.php` - Lists all rooms in a building
- `AvailabilityCalendar.php` - Mini calendar with booking status
- All widgets extend `WP_Widget` with form/update/widget methods
- Auto-detection of current building/room from page context
- Created `src/Blocks/BlockRegistrar.php` class
- Five Gutenberg blocks: Building, Room, Room Search, Buildings List, Rooms List
- Server-side rendering using shortcode system
- Block editor assets (CSS/JS) enqueuing
- Block data localization with buildings, rooms, room types, amenities
- `render_callback` functions for each block type
- Created `assets/js/blocks-editor.js`
- Block registration using `wp.blocks.registerBlockType`
- InspectorControls for sidebar settings panels
- ServerSideRender for live preview in editor
- Attribute definitions matching shortcode parameters
- Created `assets/css/blocks-editor.css`
- Minimal editor styling for block placeholders
- Preview container styling
- Updated `assets/css/frontend.css` (~1250 lines)
- CSS custom properties for theming (colors, spacing, border-radius)
- Building and room card components
- Search form with field groups
- Results grid with responsive columns
- Calendar widget with availability states (available, booked, past, today)
- Legend styling
- Responsive breakpoints: 480px, 768px, 1024px
- Updated `assets/js/frontend.js` (~825 lines)
- `WpBnb` namespace with utility methods (ajax, formatDate, parseDate, debounce)
- `SearchForm` class: form submission, date validation, results rendering, load more
- `CalendarWidget` class: month navigation, AJAX calendar loading
- `AvailabilityForm` class: availability checking on single room pages
- `PriceCalculator` class: real-time price calculation with breakdown
- XSS-safe DOM construction using textContent instead of innerHTML
- Updated `src/Plugin.php`
- Added use statements for new frontend classes
- `init_frontend()` initializes Search, Shortcodes, BlockRegistrar
- `register_widgets()` method for widget registration
- `wp_localize_script()` adds AJAX URL, nonce, i18n strings to frontend
- Updated version to 0.6.0 in both plugin header and constant
- Updated CHANGELOG.md with comprehensive v0.6.0 release notes
- Updated PLAN.md to mark Phase 6 complete
**Learnings:**
- Server-side rendered Gutenberg blocks avoid complex build processes and ensure PHP/JS output consistency
- Shortcode system works well as render backend for blocks via `render_callback`
- Widget auto-detection from page context (`is_singular()`, `get_the_ID()`) reduces configuration
- CSS custom properties enable easy theming without modifying core styles
- AJAX nonce verification requires `wp_ajax_nopriv_` for non-logged-in users in frontend search
- Calendar data from `Availability::get_calendar_data()` provides consistent format for PHP and JS rendering
- XSS prevention in JS: use `textContent` for user data, `createElement` for structure
- Frontend components require license check (`LicenseManager::is_license_valid()`) before initialization
- Block editor requires separate script handle from frontend to avoid conflicts
**Released:**
- Committed: `864b8b2` on dev branch
- Merged to main (fast-forward)
- Tagged: `v0.6.0`
- Pushed to origin: dev, main, v0.6.0
### 2026-02-03 - Bug Fixes and Enhancements
**Completed:**
- Fixed gap between settings page tabs and tab content
- Changed `.nav-tab-wrapper` margin-bottom from 20px to 0
- Added explicit border-bottom to create seamless connection with tab content
- Added license bypass for localhost development environments
- Created `LicenseManager::is_localhost()` method
- Detects: localhost, 127.0.0.1, ::1, .local/.test/.localhost/.dev/.ddev.site domains, private IP ranges
- `is_license_valid()` now returns true for localhost environments
- Added "Development Mode" notice on license settings page and dashboard when localhost detected
- Expanded General Settings with business owner fields
- Added Address section: street, city, postal code, country
- Added Contact section: email, phone, website
- Added Social Media section: Facebook, Instagram, X (Twitter), LinkedIn, TripAdvisor
- Updated `save_general_settings()` with proper sanitization for all new fields
- Created subtabs on Pricing settings tab
- Three subtabs: Pricing Tiers, Weekend Days, Seasons
- Each subtab has its own save button and focused content
- Added CSS for subtab navigation styling
- Seasons subtab now shows priority column and direct link to Seasons Manager
- Implemented auto-updates system
- Created `src/License/Updater.php` class
- Integrates with WordPress plugin update system via `pre_set_site_transient_update_plugins`
- Provides plugin info for "View details" modal via `plugins_api` filter
- Uses license client's `checkForUpdates()` method
- Configurable check frequency (1-168 hours)
- Options for notifications enabled and auto-install enabled
- Automatic cache clearing when license settings change or after updates
- Added Updates tab to settings page
- Enable/disable update notifications
- Enable/disable automatic updates
- Configurable update check frequency
- Manual "Check for Updates" button with AJAX
- Display of last check timestamp and current version
- Reordered admin submenu for better organization
- Dashboard at top, Settings at bottom
- Logical grouping: Buildings, Rooms, Bookings, Guests, Services, Calendar, Seasons
- Fixed Booking admin issues
- Fixed auto-draft status causing type errors (check for WP_Post object)
- Fixed guest dropdown to always load existing guests
- Booking title now auto-generates with guest name and dates (room removed per user request)
- Fixed booking history display on Guest edit page
- Implemented guest auto-creation from booking form
- When new guest data is entered in booking, guest record is automatically created
- Links booking to the new guest via guest_id meta
- Added encryption for sensitive guest data
- ID/passport numbers encrypted using AES-256-CBC
- 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
- Disabled Gutenberg block editor for form-based post types
- Service, Guest, and Booking post types now use classic editor
- Added `disable_block_editor()` filter to each post type class
- Meta boxes now appear properly instead of being hidden at bottom
- Form-based interfaces are more appropriate than block editor for data entry
**Files Changed:**
- `assets/css/admin.css` - Fixed tab gap, added subtab styles, booking form styles
- `assets/js/admin.js` - AJAX update check, booking form improvements, guest auto-creation
- `src/License/Manager.php` - Added `is_localhost()` method, updated `is_license_valid()`
- `src/License/Updater.php` - New file for auto-updates with configurable settings
- `src/Plugin.php` - Business owner settings, pricing subtabs, updates tab, menu reordering
- `src/PostTypes/Booking.php` - Auto-draft fixes, title generation, guest creation, disable Gutenberg
- `src/PostTypes/Guest.php` - AES-256-CBC encryption for ID numbers, disable Gutenberg
- `src/PostTypes/Service.php` - Disable Gutenberg for classic editor UI
**Learnings:**
- WordPress nav-tab styling expects tabs and content to be flush (no margin/gap)
- Localhost detection should cover common development TLDs (.local, .test, .dev, .ddev.site)
- Private IP ranges can be detected using `FILTER_FLAG_NO_PRIV_RANGE`
- WordPress plugin updates require hooking into `pre_set_site_transient_update_plugins` and `plugins_api`
- Subtabs can be implemented with query parameters and conditional rendering within a single settings callback
- URL fields should use `esc_url_raw()` for sanitization, email fields use `sanitize_email()`
- Always check if post object is valid (`$post instanceof \WP_Post`) before accessing properties - auto-draft causes issues
- AES-256-CBC encryption with random IV provides secure storage for sensitive data
- Store IV concatenated with encrypted data (IV is not secret, just needs to be unique)
- `use_block_editor_for_post_type` filter disables Gutenberg per post type
- Post types with `show_in_rest => true` get Gutenberg by default, which hides traditional meta boxes
- Form-based admin interfaces (data entry) should use classic editor, not block editor
### 2026-02-03 - Version 0.7.0 (Contact Form 7 Integration)
**Completed:**
- Created `src/Integration/CF7.php` (~750 lines)
- Custom form tags: `[bnb_building_select]`, `[bnb_room_select]`, `[bnb_date_checkin]`, `[bnb_date_checkout]`, `[bnb_guests]`
- Server-side validation for all custom tags
- Availability validation in `wpcf7_before_send_mail` hook
- Automatic booking creation on form submission via `wpcf7_mail_sent`
- Guest record creation/linking using `find_or_create_guest()` pattern
- Custom mail tags: `[_bnb_room_name]`, `[_bnb_building_name]`, `[_bnb_calculated_price]`, `[_bnb_nights]`, `[_bnb_booking_reference]`
- Form type detection via CSS class `wp-bnb-booking-form`
- Created `assets/js/cf7-integration.js` (~230 lines)
- Building-based room filtering (rooms dropdown updates when building selected)
- Date validation (check-out after check-in, no past dates)
- Guest capacity validation against room limits
- AJAX availability checking with status display
- AJAX price calculation with formatted display
- Debounced updates to prevent excessive requests
- Created `assets/css/cf7-integration.css` (~200 lines)
- Two-column responsive form layout
- Availability status indicators (checking spinner, available checkmark, unavailable X)
- Price display formatting
- Capacity warning styling
- Dark mode support via `prefers-color-scheme`
- Print styles (hide interactive elements)
- Updated `src/Plugin.php`
- Added `use Magdev\WpBnb\Integration\CF7` import
- CF7 initialization in `init_frontend()` when WPCF7 class exists
- CF7 assets enqueuing with localized i18n strings
- Updated `README.md` with comprehensive CF7 documentation
- Custom form tags reference with options
- Example booking form template
- Example inquiry form template
- Custom mail tags documentation
**Files Created:**
- `src/Integration/CF7.php` - Main CF7 integration class
- `assets/js/cf7-integration.js` - Frontend JavaScript
- `assets/css/cf7-integration.css` - Form styling
**Learnings:**
- CF7 custom tags registered via `wpcf7_add_form_tag()` with callback functions
- Validation filters follow pattern `wpcf7_validate_{tag_name}`
- `wpcf7_before_send_mail` can abort submission by setting `$abort` to true and adding validation error
- `wpcf7_mail_sent` fires after successful email, ideal for booking creation
- Custom mail tags via `wpcf7_special_mail_tags` filter receive submission data
- Form type detection by CSS class more reliable than checking for specific tags
- Room dropdown with `data-building` attributes enables client-side filtering
- AJAX endpoints reuse existing `wp_bnb_get_availability` and `wp_bnb_calculate_price` actions
- CF7 assets should depend on `contact-form-7` script/style handles
- Guest linking uses email as unique identifier for find-or-create pattern
**Released:**
- Committed: `28350aa` on dev branch
- Merged to main (fast-forward)
- Tagged: `v0.7.0`
- Pushed to origin: dev, main, v0.7.0

53
PLAN.md
View File

@@ -149,20 +149,20 @@ This document outlines the implementation plan for the WP BnB Management plugin.
- [x] Building rooms widget
- [x] Availability calendar widget
## Phase 7: Contact Form 7 Integration (v0.7.0)
## Phase 7: Contact Form 7 Integration (v0.7.0) - Complete
### Booking Request Form
- [ ] Custom CF7 tags for rooms/dates
- [ ] Form validation
- [ ] Booking creation on submission
- [ ] Email notifications
- [x] Custom CF7 tags for rooms/dates
- [x] Form validation
- [x] Booking creation on submission
- [x] Email notifications
### Inquiry Form
- [ ] General inquiry handling
- [ ] Room-specific inquiries
- [ ] Auto-response templates
- [x] General inquiry handling
- [x] Room-specific inquiries
- [x] Auto-response templates (uses default CF7 mail templates)
## Phase 8: Dashboard & Reports (v0.8.0)
@@ -180,6 +180,17 @@ This document outlines the implementation plan for the WP BnB Management plugin.
- [ ] Guest statistics
- [ ] Export functionality (CSV, PDF)
## Phase 9: Prometheus Metrics (v0.9.0)
- [ ] Meanigful Metrics for this Plugin, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
- [ ] Example Grafana-Dashboard, see <https://src.bundespruefstelle.ch/magdev/wp-prometheus/raw/branch/main/README.md> for implementation details
- [ ] Update settings page to enable/disable metrics
## Phase 10: Security Audit (v0.10.0)
- [ ] Check for Wordpress best-practises
- [ ] Review the code for OWASP Top 10, including XSS, XSRF, SQLi and other critical threads
## Future Considerations (v1.0.0+)
### WooCommerce Integration (Optional)
@@ -286,15 +297,17 @@ The plugin will provide extensive hooks for customization:
## Version Milestones
| Version | Focus | Target |
| ------- | --------------- | -------- |
| 0.0.1 | Initial setup | Complete |
| 0.1.0 | Data structures | Complete |
| 0.2.0 | Pricing | Complete |
| 0.3.0 | Bookings | Complete |
| 0.4.0 | Guests | Complete |
| 0.5.0 | Services | Complete |
| 0.6.0 | Frontend | Complete |
| 0.7.0 | CF7 Integration | TBD |
| 0.8.0 | Dashboard | TBD |
| 1.0.0 | Stable Release | TBD |
| Version | Focus | Target |
| ------- | ------------------ | -------- |
| 0.0.1 | Initial setup | Complete |
| 0.1.0 | Data structures | Complete |
| 0.2.0 | Pricing | Complete |
| 0.3.0 | Bookings | Complete |
| 0.4.0 | Guests | Complete |
| 0.5.0 | Services | Complete |
| 0.6.0 | Frontend | Complete |
| 0.7.0 | CF7 Integration | Complete |
| 0.8.0 | Dashboard | TBD |
| 0.9.0 | Prometheus Metrics | TBD |
| 0.10.0 | Security Audit | TBD |
| 1.0.0 | Stable Release | TBD |

212
README.md
View File

@@ -10,17 +10,22 @@ 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 and inquiries through CF7 forms
### Requirements
- WordPress 6.0 or higher
- PHP 8.3 or higher
- Valid license key
- Contact Form 7 (optional, for booking forms)
## Installation
@@ -44,6 +49,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 +103,192 @@ 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
## Contact Form 7 Integration
The plugin integrates with Contact Form 7 to accept booking requests and inquiries. Custom form tags are provided for room selection, date pickers, and guest counts.
### Custom Form Tags
Use these tags in your CF7 forms:
- `[bnb_building_select name]` - Building dropdown (optional filter for rooms)
- `[bnb_room_select* name]` - Room dropdown with capacity data
- `[bnb_date_checkin* name]` - Check-in date picker
- `[bnb_date_checkout* name]` - Check-out date picker
- `[bnb_guests* name]` - Guest count input
### Tag Options
**`[bnb_building_select]`**:
- `first_as_label:"text"` - Placeholder text (default: "All Locations")
**`[bnb_room_select]`**:
- `building_field:"name"` - Link to building field for filtering
- `first_as_label:"text"` - Placeholder text (default: "Select Room")
**`[bnb_guests]`**:
- `min:N` - Minimum guests (default: 1)
- `max:N` - Maximum guests (default: 10)
- `default:N` - Default value (default: 1)
### Example Booking Form
```txt
<div class="wp-bnb-booking-form">
<h3>Book Your Stay</h3>
<div class="wp-bnb-form-row">
[bnb_building_select building first_as_label:"All Locations"]
</div>
<div class="wp-bnb-form-row">
[bnb_room_select* room building_field:"building" first_as_label:"Select a Room"]
</div>
<div class="wp-bnb-form-row-2col">
<div class="wp-bnb-form-field">
<label>Check-in</label>
[bnb_date_checkin* check_in]
</div>
<div class="wp-bnb-form-field">
<label>Check-out</label>
[bnb_date_checkout* check_out]
</div>
</div>
<div class="wp-bnb-availability-status"></div>
<div class="wp-bnb-form-row">
<label>Number of Guests</label>
[bnb_guests* guests min:1 max:10 default:2]
</div>
<div class="wp-bnb-price-display"></div>
<h4>Your Information</h4>
<div class="wp-bnb-form-row-2col">
<div class="wp-bnb-form-field">
<label>First Name</label>
[text* first_name]
</div>
<div class="wp-bnb-form-field">
<label>Last Name</label>
[text* last_name]
</div>
</div>
<div class="wp-bnb-form-row">
<label>Email</label>
[email* your_email]
</div>
<div class="wp-bnb-form-row">
<label>Phone</label>
[tel your_phone]
</div>
<div class="wp-bnb-form-row">
<label>Message</label>
[textarea your_message]
</div>
[submit "Request Booking"]
</div>
```
### Example Inquiry Form
For room-specific inquiries, add the `wp-bnb-inquiry-form` class:
```txt
<div class="wp-bnb-inquiry-form">
<h3>Inquire About This Room</h3>
[hidden room default:123]
<div class="wp-bnb-form-row">
<label>Your Name</label>
[text* your_name]
</div>
<div class="wp-bnb-form-row">
<label>Email</label>
[email* your_email]
</div>
<div class="wp-bnb-form-row">
<label>Your Question</label>
[textarea* your_message]
</div>
[submit "Send Inquiry"]
</div>
```
### Form Features
- **Availability Checking**: Real-time AJAX validation shows room availability
- **Price Display**: Estimated total calculated and displayed automatically
- **Room Filtering**: Rooms filter by building selection
- **Date Validation**: Check-out must be after check-in, no past dates
- **Capacity Validation**: Guest count validated against room capacity
- **Automatic Booking**: Booking record created with "pending" status on submission
- **Guest Linking**: Guest records created or linked by email address
### Custom Mail Tags
Use these in your CF7 mail templates:
- `[_bnb_room_name]` - Room title
- `[_bnb_building_name]` - Building name
- `[_bnb_calculated_price]` - Formatted price
- `[_bnb_nights]` - Number of nights
- `[_bnb_booking_reference]` - Booking reference (after creation)
## Hooks and Filters
@@ -123,7 +311,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 +325,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.

View File

@@ -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;

View File

@@ -0,0 +1,344 @@
/**
* WP BnB Contact Form 7 Integration Styles
*
* Styling for CF7 booking forms.
*
* @package Magdev\WpBnb
*/
/* Custom Properties */
:root {
--wp-bnb-cf7-primary: #2271b1;
--wp-bnb-cf7-success: #00a32a;
--wp-bnb-cf7-warning: #dba617;
--wp-bnb-cf7-error: #d63638;
--wp-bnb-cf7-text: #1d2327;
--wp-bnb-cf7-text-light: #646970;
--wp-bnb-cf7-border: #c3c4c7;
--wp-bnb-cf7-bg: #f0f0f1;
--wp-bnb-cf7-radius: 4px;
--wp-bnb-cf7-spacing: 1rem;
}
/* Form Layout */
.wp-bnb-booking-form,
.wp-bnb-inquiry-form {
max-width: 700px;
margin: 0 auto;
}
.wp-bnb-booking-form h3,
.wp-bnb-booking-form h4,
.wp-bnb-inquiry-form h3,
.wp-bnb-inquiry-form h4 {
margin-top: 1.5em;
margin-bottom: 0.75em;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--wp-bnb-cf7-border);
}
.wp-bnb-booking-form h3:first-child,
.wp-bnb-inquiry-form h3:first-child {
margin-top: 0;
}
/* Form Rows */
.wp-bnb-form-row {
margin-bottom: var(--wp-bnb-cf7-spacing);
}
.wp-bnb-form-row-2col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--wp-bnb-cf7-spacing);
}
@media (max-width: 480px) {
.wp-bnb-form-row-2col {
grid-template-columns: 1fr;
}
}
/* Form Fields */
.wp-bnb-form-field {
display: flex;
flex-direction: column;
}
.wp-bnb-form-field label {
display: block;
margin-bottom: 0.25rem;
font-weight: 600;
font-size: 0.875rem;
color: var(--wp-bnb-cf7-text);
}
/* Custom CF7 Tags Styling */
.wp-bnb-building-select,
.wp-bnb-room-select,
.wp-bnb-date-checkin,
.wp-bnb-date-checkout,
.wp-bnb-guests {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border: 1px solid var(--wp-bnb-cf7-border);
border-radius: var(--wp-bnb-cf7-radius);
background-color: #fff;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.wp-bnb-building-select:focus,
.wp-bnb-room-select:focus,
.wp-bnb-date-checkin:focus,
.wp-bnb-date-checkout:focus,
.wp-bnb-guests:focus {
outline: none;
border-color: var(--wp-bnb-cf7-primary);
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.25);
}
/* Select dropdown */
.wp-bnb-room-select optgroup {
font-weight: 600;
font-style: normal;
color: var(--wp-bnb-cf7-text);
}
/* Date inputs */
.wp-bnb-date-checkin,
.wp-bnb-date-checkout {
cursor: pointer;
}
/* Number input */
.wp-bnb-guests {
max-width: 120px;
}
/* Availability Status */
.wp-bnb-availability-status {
padding: var(--wp-bnb-cf7-spacing);
margin: var(--wp-bnb-cf7-spacing) 0;
background-color: var(--wp-bnb-cf7-bg);
border-radius: var(--wp-bnb-cf7-radius);
text-align: center;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.wp-bnb-availability-status:empty {
display: none;
}
.wp-bnb-checking {
color: var(--wp-bnb-cf7-text-light);
font-style: italic;
}
.wp-bnb-checking::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid var(--wp-bnb-cf7-border);
border-top-color: var(--wp-bnb-cf7-primary);
border-radius: 50%;
animation: wp-bnb-spin 0.8s linear infinite;
vertical-align: middle;
}
@keyframes wp-bnb-spin {
to {
transform: rotate(360deg);
}
}
.wp-bnb-available {
color: var(--wp-bnb-cf7-success);
font-weight: 600;
}
.wp-bnb-available::before {
content: "\2713";
display: inline-block;
margin-right: 8px;
font-size: 1.25em;
}
.wp-bnb-unavailable {
color: var(--wp-bnb-cf7-error);
font-weight: 600;
}
.wp-bnb-unavailable::before {
content: "\2717";
display: inline-block;
margin-right: 8px;
font-size: 1.25em;
}
/* Price Display */
.wp-bnb-price-display {
padding: var(--wp-bnb-cf7-spacing);
margin: var(--wp-bnb-cf7-spacing) 0;
background-color: #e7f5ea;
border: 1px solid var(--wp-bnb-cf7-success);
border-radius: var(--wp-bnb-cf7-radius);
text-align: center;
}
.wp-bnb-price-display:empty {
display: none;
}
.wp-bnb-price-label {
color: var(--wp-bnb-cf7-text-light);
font-size: 0.875rem;
}
.wp-bnb-price-amount {
font-size: 1.5rem;
font-weight: 700;
color: var(--wp-bnb-cf7-success);
margin: 0 0.5rem;
}
.wp-bnb-nights {
color: var(--wp-bnb-cf7-text-light);
font-size: 0.875rem;
}
/* Capacity Warning */
.wp-bnb-capacity-warning {
display: block;
margin-top: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.8125rem;
color: var(--wp-bnb-cf7-error);
background-color: #fcf0f1;
border-radius: var(--wp-bnb-cf7-radius);
}
/* Validation Errors */
.wpcf7-form-control-wrap .wpcf7-not-valid-tip {
color: var(--wp-bnb-cf7-error);
font-size: 0.8125rem;
margin-top: 0.25rem;
}
.wpcf7-form-control.wpcf7-not-valid {
border-color: var(--wp-bnb-cf7-error);
}
/* Response Messages */
.wpcf7 form.sent .wpcf7-response-output {
border-color: var(--wp-bnb-cf7-success);
background-color: #e7f5ea;
color: var(--wp-bnb-cf7-success);
}
.wpcf7 form.failed .wpcf7-response-output,
.wpcf7 form.aborted .wpcf7-response-output,
.wpcf7 form.spam .wpcf7-response-output,
.wpcf7 form.invalid .wpcf7-response-output {
border-color: var(--wp-bnb-cf7-error);
background-color: #fcf0f1;
color: var(--wp-bnb-cf7-error);
}
/* Submit Button */
.wp-bnb-booking-form .wpcf7-submit,
.wp-bnb-inquiry-form .wpcf7-submit {
display: inline-block;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
color: #fff;
background-color: var(--wp-bnb-cf7-primary);
border: none;
border-radius: var(--wp-bnb-cf7-radius);
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.wp-bnb-booking-form .wpcf7-submit:hover,
.wp-bnb-inquiry-form .wpcf7-submit:hover {
background-color: #135e96;
}
.wp-bnb-booking-form .wpcf7-submit:disabled,
.wp-bnb-inquiry-form .wpcf7-submit:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* Spinner */
.wpcf7 .wpcf7-spinner {
margin-left: 0.5rem;
}
/* Hidden Room Field (for inquiry forms) */
.wp-bnb-inquiry-form input[type="hidden"] + .wpcf7-form-control-wrap {
display: none;
}
/* Form Section Headers */
.wp-bnb-booking-form hr,
.wp-bnb-inquiry-form hr {
margin: 1.5rem 0;
border: none;
border-top: 1px solid var(--wp-bnb-cf7-border);
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--wp-bnb-cf7-text: #f0f0f1;
--wp-bnb-cf7-text-light: #a7aaad;
--wp-bnb-cf7-border: #50575e;
--wp-bnb-cf7-bg: #2c3338;
}
.wp-bnb-building-select,
.wp-bnb-room-select,
.wp-bnb-date-checkin,
.wp-bnb-date-checkout,
.wp-bnb-guests {
background-color: #3c434a;
color: var(--wp-bnb-cf7-text);
}
.wp-bnb-price-display {
background-color: #1a3320;
border-color: var(--wp-bnb-cf7-success);
}
.wp-bnb-capacity-warning {
background-color: #3c1618;
}
.wpcf7 form.sent .wpcf7-response-output {
background-color: #1a3320;
}
.wpcf7 form.failed .wpcf7-response-output,
.wpcf7 form.aborted .wpcf7-response-output,
.wpcf7 form.spam .wpcf7-response-output,
.wpcf7 form.invalid .wpcf7-response-output {
background-color: #3c1618;
}
}
/* Print Styles */
@media print {
.wp-bnb-availability-status,
.wp-bnb-price-display,
.wpcf7-submit {
display: none;
}
}

View File

@@ -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();

View File

@@ -0,0 +1,375 @@
/**
* WP BnB Contact Form 7 Integration
*
* Handles dynamic form behavior for booking forms.
*
* @package Magdev\WpBnb
*/
(function() {
'use strict';
const WpBnbCF7 = {
config: window.wpBnbCF7 || {},
/**
* Initialize all CF7 integration features.
*/
init: function() {
this.initBuildingRoomFilter();
this.initDateValidation();
this.initCapacityValidation();
this.initAvailabilityCheck();
this.initPriceDisplay();
},
/**
* Filter rooms dropdown when building is selected.
*/
initBuildingRoomFilter: function() {
document.querySelectorAll('[data-bnb-building-select]').forEach(function(buildingSelect) {
const form = buildingSelect.closest('form');
const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
if (!roomSelect) return;
// Store all options for filtering
const allOptions = Array.from(roomSelect.querySelectorAll('option, optgroup'));
const originalHTML = roomSelect.innerHTML;
buildingSelect.addEventListener('change', function() {
const selectedBuilding = buildingSelect.value;
// Show all options if no building selected
if (!selectedBuilding) {
roomSelect.innerHTML = originalHTML;
roomSelect.dispatchEvent(new Event('change'));
return;
}
// Filter options by building
roomSelect.innerHTML = '';
// Add placeholder option
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = WpBnbCF7.config.i18n?.selectRoom || '-- Select Room --';
roomSelect.appendChild(placeholder);
allOptions.forEach(function(el) {
if (el.tagName === 'OPTGROUP') {
// Check if any options in this optgroup match
const matchingOptions = Array.from(el.querySelectorAll('option')).filter(function(opt) {
return opt.dataset.building === selectedBuilding;
});
if (matchingOptions.length > 0) {
const clonedGroup = el.cloneNode(false);
matchingOptions.forEach(function(opt) {
clonedGroup.appendChild(opt.cloneNode(true));
});
roomSelect.appendChild(clonedGroup);
}
}
});
// Trigger change to update dependent fields
roomSelect.dispatchEvent(new Event('change'));
});
});
},
/**
* Validate and link check-in/check-out dates.
*/
initDateValidation: function() {
document.querySelectorAll('[data-bnb-checkin]').forEach(function(checkinInput) {
const form = checkinInput.closest('form');
const checkoutInput = form ? form.querySelector('[data-bnb-checkout]') : null;
if (!checkoutInput) return;
// Set minimum check-in to today
const today = WpBnbCF7.formatDate(new Date());
if (!checkinInput.getAttribute('min') || checkinInput.getAttribute('min') < today) {
checkinInput.setAttribute('min', today);
}
checkinInput.addEventListener('change', function() {
if (checkinInput.value) {
// Set checkout minimum to checkin + 1 day
const minCheckout = new Date(checkinInput.value);
minCheckout.setDate(minCheckout.getDate() + 1);
checkoutInput.setAttribute('min', WpBnbCF7.formatDate(minCheckout));
// Clear checkout if it's now invalid
if (checkoutInput.value && checkoutInput.value <= checkinInput.value) {
checkoutInput.value = '';
}
// Trigger availability check
WpBnbCF7.triggerAvailabilityCheck(form);
}
});
checkoutInput.addEventListener('change', function() {
if (checkoutInput.value && checkinInput.value) {
if (checkoutInput.value <= checkinInput.value) {
alert(WpBnbCF7.config.i18n?.invalidDateRange || 'Check-out must be after check-in');
checkoutInput.value = '';
return;
}
// Trigger availability check
WpBnbCF7.triggerAvailabilityCheck(form);
}
});
});
},
/**
* Validate guest count against room capacity.
*/
initCapacityValidation: function() {
document.querySelectorAll('[data-bnb-guests]').forEach(function(guestsInput) {
const form = guestsInput.closest('form');
const roomSelect = form ? form.querySelector('[data-bnb-room-select]') : null;
if (!roomSelect) return;
const validateCapacity = function() {
const selectedOption = roomSelect.selectedOptions[0];
const capacity = parseInt(selectedOption?.dataset.capacity || 99, 10);
const guests = parseInt(guestsInput.value || 0, 10);
// Update max attribute
guestsInput.setAttribute('max', capacity);
// Show warning if over capacity
const wrapper = guestsInput.closest('.wpcf7-form-control-wrap');
let warning = wrapper ? wrapper.querySelector('.wp-bnb-capacity-warning') : null;
if (guests > capacity) {
if (!warning && wrapper) {
warning = document.createElement('span');
warning.className = 'wp-bnb-capacity-warning';
wrapper.appendChild(warning);
}
if (warning) {
warning.textContent = (WpBnbCF7.config.i18n?.capacityExceeded || 'Maximum %d guests for this room').replace('%d', capacity);
}
} else if (warning) {
warning.remove();
}
};
roomSelect.addEventListener('change', validateCapacity);
guestsInput.addEventListener('change', validateCapacity);
guestsInput.addEventListener('input', validateCapacity);
});
},
/**
* Initialize AJAX availability checking.
*/
initAvailabilityCheck: function() {
// Find forms with availability display
document.querySelectorAll('.wp-bnb-availability-status').forEach(function(statusEl) {
const form = statusEl.closest('form');
if (form) {
form._availabilityStatus = statusEl;
}
});
},
/**
* Trigger availability check for a form.
*
* @param {HTMLFormElement} form Form element.
*/
triggerAvailabilityCheck: function(form) {
const roomSelect = form.querySelector('[data-bnb-room-select]');
const checkinInput = form.querySelector('[data-bnb-checkin]');
const checkoutInput = form.querySelector('[data-bnb-checkout]');
const statusEl = form._availabilityStatus || form.querySelector('.wp-bnb-availability-status');
const priceEl = form.querySelector('.wp-bnb-price-display');
if (!roomSelect || !checkinInput || !checkoutInput) return;
const roomId = roomSelect.value;
const checkIn = checkinInput.value;
const checkOut = checkoutInput.value;
if (!roomId || !checkIn || !checkOut) {
if (statusEl) statusEl.innerHTML = '';
if (priceEl) priceEl.innerHTML = '';
return;
}
// Show loading state
if (statusEl) {
statusEl.innerHTML = '<span class="wp-bnb-checking">' + (WpBnbCF7.config.i18n?.checking || 'Checking availability...') + '</span>';
}
// Make AJAX request
WpBnbCF7.ajax('wp_bnb_get_availability', {
room_id: roomId,
check_in: checkIn,
check_out: checkOut
})
.then(function(response) {
if (statusEl) {
if (response.available) {
let html = '<span class="wp-bnb-available">' + (WpBnbCF7.config.i18n?.available || 'Room is available!') + '</span>';
statusEl.innerHTML = html;
} else {
statusEl.innerHTML = '<span class="wp-bnb-unavailable">' + (WpBnbCF7.config.i18n?.unavailable || 'Room is not available for these dates') + '</span>';
}
}
// Update price display
if (priceEl && response.available && response.price_formatted) {
priceEl.innerHTML = '<span class="wp-bnb-price-label">' + (WpBnbCF7.config.i18n?.estimatedTotal || 'Estimated Total') + ':</span> ' +
'<span class="wp-bnb-price-amount">' + response.price_formatted + '</span> ' +
'<span class="wp-bnb-nights">(' + response.nights + ' ' + (WpBnbCF7.config.i18n?.nights || 'nights') + ')</span>';
} else if (priceEl) {
priceEl.innerHTML = '';
}
})
.catch(function(error) {
console.error('Availability check failed:', error);
if (statusEl) {
statusEl.innerHTML = '';
}
});
},
/**
* Initialize price display updates.
*/
initPriceDisplay: function() {
const self = this;
document.querySelectorAll('.wp-bnb-price-display').forEach(function(priceEl) {
const form = priceEl.closest('form');
if (!form) return;
const updatePrice = self.debounce(function() {
const roomSelect = form.querySelector('[data-bnb-room-select]');
const checkinInput = form.querySelector('[data-bnb-checkin]');
const checkoutInput = form.querySelector('[data-bnb-checkout]');
if (!roomSelect?.value || !checkinInput?.value || !checkoutInput?.value) {
priceEl.innerHTML = '';
return;
}
self.ajax('wp_bnb_calculate_price', {
room_id: roomSelect.value,
check_in: checkinInput.value,
check_out: checkoutInput.value
})
.then(function(response) {
priceEl.innerHTML = '<span class="wp-bnb-price-label">' + (self.config.i18n?.estimatedTotal || 'Estimated Total') + ':</span> ' +
'<span class="wp-bnb-price-amount">' + response.price_formatted + '</span> ' +
'<span class="wp-bnb-nights">(' + response.nights + ' ' + (self.config.i18n?.nights || 'nights') + ')</span>';
})
.catch(function() {
priceEl.innerHTML = '';
});
}, 500);
// Bind to relevant field changes
form.querySelectorAll('[data-bnb-room-select], [data-bnb-checkin], [data-bnb-checkout]')
.forEach(function(input) {
input.addEventListener('change', updatePrice);
});
});
},
/**
* Make AJAX request.
*
* @param {string} action AJAX action name.
* @param {object} data Request data.
* @return {Promise}
*/
ajax: function(action, data) {
data = data || {};
const formData = new FormData();
formData.append('action', action);
formData.append('nonce', this.config.nonce || '');
Object.keys(data).forEach(function(key) {
if (data[key] !== null && data[key] !== undefined) {
formData.append(key, data[key]);
}
});
return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(function(response) {
return response.json();
})
.then(function(responseData) {
if (!responseData.success) {
throw new Error(responseData.data?.message || 'Request failed');
}
return responseData.data;
});
},
/**
* Format date as YYYY-MM-DD.
*
* @param {Date} date Date object.
* @return {string}
*/
formatDate: function(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
},
/**
* Debounce function.
*
* @param {function} func Function to debounce.
* @param {number} wait Milliseconds to wait.
* @return {function}
*/
debounce: function(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
WpBnbCF7.init();
});
} else {
WpBnbCF7.init();
}
// Re-initialize on CF7 form reset
document.addEventListener('wpcf7reset', function(event) {
setTimeout(function() {
WpBnbCF7.init();
}, 100);
});
// Export to window for external access
window.WpBnbCF7 = WpBnbCF7;
})();

1654
src/Integration/CF7.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

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

View File

@@ -16,10 +16,13 @@ use Magdev\WpBnb\Booking\Availability;
use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\Frontend\Search;
use Magdev\WpBnb\Frontend\Shortcodes;
use Magdev\WpBnb\Integration\CF7;
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 +131,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 +145,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 +166,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.
@@ -183,6 +203,11 @@ final class Plugin {
// Register widgets.
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
// Initialize Contact Form 7 integration if CF7 is active.
if ( class_exists( 'WPCF7' ) ) {
CF7::init();
}
}
/**
@@ -280,6 +305,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' ),
),
)
);
@@ -339,6 +368,43 @@ final class Plugin {
),
)
);
// Load CF7 integration assets if CF7 is active.
if ( class_exists( 'WPCF7' ) ) {
wp_enqueue_style(
'wp-bnb-cf7',
WP_BNB_URL . 'assets/css/cf7-integration.css',
array( 'contact-form-7' ),
WP_BNB_VERSION
);
wp_enqueue_script(
'wp-bnb-cf7',
WP_BNB_URL . 'assets/js/cf7-integration.js',
array( 'contact-form-7' ),
WP_BNB_VERSION,
true
);
wp_localize_script(
'wp-bnb-cf7',
'wpBnbCF7',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
'i18n' => array(
'selectRoom' => __( '-- Select Room --', 'wp-bnb' ),
'checking' => __( 'Checking availability...', 'wp-bnb' ),
'available' => __( 'Room is available!', 'wp-bnb' ),
'unavailable' => __( 'Room is not available for these dates', 'wp-bnb' ),
'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
'capacityExceeded' => __( 'Maximum %d guests for this room', 'wp-bnb' ),
'estimatedTotal' => __( 'Estimated Total', 'wp-bnb' ),
'nights' => __( 'nights', 'wp-bnb' ),
),
)
);
}
}
/**
@@ -392,6 +458,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 +527,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 +592,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 +607,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 +630,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 +669,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 +817,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 +834,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 +998,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 +1097,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 +1319,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 +1334,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 +1342,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 +1423,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.
*

View File

@@ -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.
*

View File

@@ -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 );
}
}

View File

@@ -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;
}
/**

View File

@@ -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.7.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.7.1' );
// Plugin path constants.
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );