Files
2026-05-06 00:18:37 +02:00

190 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Technical Concerns
**Analysis Date:** 2026-05-05
**Revised:** 2026-05-05 — verified call sites; demoted false positives (see CHANGELOG at bottom)
Prioritized list of issues found during codebase analysis.
---
## CRITICAL — Security / Data Loss Risk
### C1. Race condition on booking creation — no atomic lock
**Files:** `includes/class-availability.php` (`is_available`), `api/class-rest-controller.php` (`create_booking` lines 326362)
Flow: `is_available()``Booking::create()``mark_as_booked()` — three separate non-transactional operations. Two concurrent POST requests for the same yacht and overlapping dates will both pass the check before either writes. The `UNIQUE KEY yacht_date (yacht_id, date)` will reject the second `mark_as_booked()` iteration silently, but both `yacht_booking` posts are already created. Admin sees two confirmed bookings for the same dates.
**Fix:** Wrap in a DB transaction with `SELECT ... FOR UPDATE` on availability rows, or insert a sentinel lock row first and check the result before proceeding.
---
### C2. OAuth credentials stored unencrypted in `wp_options`
**File:** `integrations/google-calendar/class-oauth-handler.php` lines 185187, 213215
`yacht_booking_gcal_credentials` (client_id, client_secret) and `yacht_booking_gcal_tokens` (access_token, refresh_token) stored as plaintext serialized arrays. Any plugin or user with DB read access can extract a working Google Calendar refresh token. If the database is compromised (via SQL injection elsewhere on the WordPress install or hosting breach), the Google Calendar account is fully exposed.
**Fix:** Encrypt with WordPress secret keys (`AUTH_KEY` etc.) before storing; decrypt on read.
---
## HIGH — Reliability
### H1. GCal API HTTP errors silently ignored
**Files:** `integrations/google-calendar/class-gcal-service.php`, `class-oauth-handler.php`
Every API call checks `is_wp_error()` but not the HTTP response code. A `401`, `403`, `429`, or `500` from Google is treated as success — `json_decode` runs, expected fields like `$body['id']` or `$body['access_token']` are absent, function returns `false`. All errors only logged when `WP_DEBUG === true`. In production: complete silence.
---
### H2. Token refresh has no concurrency protection
**File:** `integrations/google-calendar/class-oauth-handler.php` lines 107128
Simultaneous WP-Cron events both finding the token expired will both call `refresh_access_token()`. Google may invalidate the first new token when the second refresh request arrives. Result: permanently broken GCal connection requiring manual reauth.
**Fix:** Wrap refresh in a transient-based lock (`set_transient('gcal_refresh_lock', 1, 60)`), or use `wp_cache_add()` semantics.
---
### H3. `register_cron_actions()` may not be called — cron hooks fire with no registered callbacks
**File:** `integrations/google-calendar/class-sync-controller.php` lines 141153
The constructor registers `on_booking_created` etc., but `register_cron_actions()` (which registers the `yacht_booking_sync_to_gcal` action itself) is a separate static method. Need to verify it is called on every `init` / `plugins_loaded` request. If not, scheduled single events silently fire with no handler.
---
### H4. Single calendar for all yachts — GCal events apply to every yacht
**File:** `integrations/google-calendar/class-gcal-service.php` lines 196272
`get_calendar_id()` returns one shared option (`yacht_booking_gcal_calendar_id`). `pull_from_gcal()` calls `sync_from_gcal($yacht->ID)` for each yacht but each call fetches all events from the same calendar and blocks that yacht for all those dates. Multi-yacht sites will incorrectly block all yachts for every GCal event.
---
### H5. `tmp-fix-polish.php` in project root reveals dev paths
**File:** `tmp-fix-polish.php`
Contains only one line: a path string to another local project (`c:\visual studio code\projekty\jachty.pagedev.pl\tmp-fix-polish.php`). No PHP tag, no code. If deployed via FTP, the raw path string is publicly accessible at the URL. Should be deleted and excluded from deployment.
---
## MEDIUM — Maintainability / Correctness
### M1. Date validation missing on booking creation
**File:** `api/class-rest-controller.php` lines 116122
`start_date`/`end_date` use only `sanitize_text_field` — no `validate_callback`. An invalid date string like `"not-a-date"` reaches `new \DateTime()` in `Availability::count_days()` without try/catch, producing an uncaught fatal error visible to the public. The `/availability` endpoint validates dates correctly; `/bookings` does not.
**Fix:** Add `validate_callback` checking `\DateTime::createFromFormat('Y-m-d', $param) !== false`.
---
### M2. No pagination on `get_yachts` and `get_bookings`
**File:** `api/class-rest-controller.php` lines 222245, 429462
Both endpoints use `posts_per_page => -1`. Will degrade progressively as bookings accumulate.
---
### M3. N+1 query pattern in booking and yacht lists
**File:** `api/class-rest-controller.php` lines 442458
For each booking: `get_post($yacht_id)` called individually inside loop. For each yacht: three separate `get_post_meta()` calls per row. With 20 bookings: 60+ queries per response.
---
### M4. iCal importer ignores `STATUS:CANCELLED` events
**File:** `integrations/ical/class-ical-import.php` `parse_ics()` method
Parser reads UID, SUMMARY, DTSTART, DTEND but ignores STATUS. Cancelled events in the iCal feed are imported as blocks. The GCal service correctly skips `status: cancelled` events (line 347 of `class-gcal-service.php`) — parity needed in iCal importer.
---
### M5. `mark_as_blocked()` passes `null` booking_id with `%d` format
**File:** `includes/class-availability.php` lines 98109
`$wpdb->replace()` receives `null` with `%d` format specifier. In older MySQL/MariaDB this may insert `0` instead of `NULL`, making it impossible to distinguish a blocked row from one linked to booking ID 0.
---
### M6. OAuth redirect URI hardcoded to production domain
**File:** `integrations/google-calendar/class-oauth-handler.php` lines 40, 72
`$redirect_uri = 'https://jachty.pagedev.pl/wp-admin/...'` — literal string, not `admin_url()`. OAuth fails completely on staging or local dev. Comment acknowledges this is intentional but makes testing and domain migration fragile.
---
## LOW — Cleanup / Polish
### L1. Dead code: `Availability::mark_as_available()` has bug but is never called
**File:** `includes/class-availability.php` lines 123149
The function ignores its `$start_date`/`$end_date` parameters and runs `$wpdb->delete()` filtered only by `yacht_id` — which would wipe the entire availability table for that yacht.
**Verification:** `grep` across the plugin shows zero call sites. All code that handles cancellation/deletion uses `clear_booking_availability($booking_id)` (in `Admin`, `Rest_Controller`, `GCal_Service`, `ICal_Import`), which correctly filters by `booking_id`.
**Risk:** Trap for a future developer who reads the method name and decides to use it. Recommend either deleting the function or replacing its body with the commented-out correct implementation that already lives below it (lines 137146).
---
### L2. Dead form field: `yacht_booking_nonce` from `wp_nonce_field` is never read
**Files:** `frontend/class-shortcode.php:329`, `frontend/class-calendar-widget.php:429`
The booking form renders `wp_nonce_field('yacht_booking_submit', 'yacht_booking_nonce')` as a hidden input, but `calendar.js` (lines 336343) builds `formData` manually from named fields and never includes it. The actual nonce travels via `X-WP-Nonce` header from `yachtBookingData.nonce` (a `wp_create_nonce('wp_rest')` value), which matches what `Rest_Controller::create_booking()` verifies.
**Risk:** Code rot — looks like it's doing something but isn't. Either remove the `wp_nonce_field` call, or wire JS to send it as a fallback for the no-JS case (currently there is no no-JS fallback anyway).
---
### L3. Logging disabled in production
All `error_log()` calls gated on `WP_DEBUG === true`. GCal sync errors, token refresh failures, and iCal import problems are silently discarded in production. Consider a persistent log table or option-based error queue surfaced in admin.
---
### L4. No incremental DB migration path
**File:** `includes/class-installer.php`
No `upgrade()` method runs when `yacht_booking_version` in `wp_options` differs from `YACHT_BOOKING_VERSION`. `dbDelta()` adds missing columns but won't remove old ones or modify existing column types. Future schema changes risk silent failures on existing installs.
---
### L5. `yacht_booking` CPT exposed to default WP REST API
**File:** `includes/class-booking.php` line 48
`show_in_rest => true` registers the CPT at `/wp/v2/yacht_booking`. The plugin's own `/yacht-booking/v1/` namespace is the intended REST surface. Set `show_in_rest => false` unless there is a reason for the default WP exposure.
---
### L6. `Booking::create()` does not validate end > start
**File:** `includes/class-booking.php` lines 229230
A booking with `end_date` before `start_date` is stored without error. `get_date_range()` while-loop exits immediately, no availability rows are created, and the booking is invisible in the calendar but exists as a CPT post.
---
## CHANGELOG
**2026-05-05 (revision):** Subagent audit produced two false positives that were initially listed as CRITICAL. Verified by tracing call sites:
- **Old C1** (`mark_as_available` deletes all rows) → demoted to **L1**. The function is never called anywhere in the plugin; all cancellation paths use the correct `clear_booking_availability($booking_id)`. Remains a trap for future devs but no production impact.
- **Old C3** (POST /bookings nonce mismatch) → demoted to **L2**. The actual auth path (JS → `X-WP-Nonce` header → `wp_verify_nonce(..., 'wp_rest')`) is fully consistent. The `wp_nonce_field('yacht_booking_submit', ...)` in shortcode/widget renders a hidden input that JS never reads — dead form field, not a security gap.
Race condition (old C2 → C1) and unencrypted OAuth tokens (old C4 → C2) remain valid CRITICAL items.