Follow-up demo feedback: the My Availability page now opens in its weekly
calendar (usc_view=list opts back into the table, which keeps the bulk-delete
form), matching the new lessons defaults.
Co-Authored-By: Claude Fable 5 <[email protected]>
The list view of Current Slots gets a checkbox per unbooked slot, a
select-all header checkbox, and a Delete selected button submitting a
new bulk_delete form action. Each id is ownership-checked through the
same path as single delete; the repository's is_booked guard refuses
booked slots as a second layer. Row checkboxes attach to the bulk form
via the HTML form attribute because the table already contains the
per-row delete forms and forms cannot nest.
Closes#57
Co-Authored-By: Claude Fable 5 <[email protected]>
Availability windows were stored and served as a single bookable row, so a
9:00 AM-4:00 PM window showed to students as one giant slot and booking it
consumed the whole day; past and multi-day windows also leaked into the
booking page as nonsense entries.
- Split windows into consecutive lesson-length slots on save (REST and admin
form); each chunk is independently bookable and weekly recurrence creates a
series per chunk so "reserve this time weekly" holds the same hour each week
- Reject windows spanning multiple days or shorter than the lesson length
(400 invalid_window)
- Never return slots whose start has passed from GET /availability
- Migrate pre-split rows: Plugin::boot re-runs the Installer on version change
and AvailabilityRepository::splitOversizedWindows() rewrites unbooked
same-day oversized windows in place
- Display all times in 12-hour AM/PM form (booking page, wp-admin lists,
editor previews)
- Add a List | Week view toggle to the student booking page and the
instructor availability page, with previous/next-week navigation honouring
the site's start_of_week option (new WeekCalendar helper)
Co-Authored-By: Claude Fable 5 <[email protected]>
- Bump phpstan/phpstan ^2.0 and szepeviktor/phpstan-wordpress ^2.0
- Move the analysis level into phpstan.neon (single source) and raise it to 10
- Add Val, a runtime coercion helper that narrows untyped WordPress boundary
values (wpdb rows, REST params, superglobals, options) with explicit checks
instead of blind casts, plus unit tests
- Type value-object fromRow() params as stdClass (what wpdb returns) and map
columns through Val so unexpected shapes degrade safely
- Use %i identifier placeholders for table names in all wpdb::prepare() calls
so every query string is a literal and identifiers are escaped by WordPress;
raises the minimum WordPress version to 6.2 where %i was introduced
- Guard wpdb::prepare() null result before wpdb::query() in updateTax()
- Fix nullable get_permalink()/strtotime() handling, list types at REST and
capability call sites, dead null-coalescing on checked superglobals, and
narrow get_users() results before mapping
- Register Val method names with the ValidatedSanitizedInput sniff so it
validates the real sanitizer around each superglobal read
- Update repository unit tests for the %i placeholder arguments
Co-Authored-By: Claude Fable 5 <[email protected]>
Four fixes from a security review pass:
- Neutralise CSV formula injection in the payments export: fields with a
leading =, +, -, @, tab, or CR (e.g. a hostile student display name) are
apostrophe-prefixed in PaymentReport::csvLine() so they open as text in
Excel/Google Sheets. Fixes#39.
- Sanitise policy bodies with wp_kses_post at output in
PolicyEndpoint::index() (the booking JS renders that HTML raw), so a
future write path that forgets kses can never become stored XSS.
Fixes#40.
- Store invite tokens hashed (SHA-256) at rest: a database leak can no
longer redeem pending invites. The registration link is shown once, at
creation; the pending list shows email/invited date; lookups hash the
submitted token. Existing plaintext pending invites must be re-issued.
Fixes#41.
- Validate availability slot datetimes on both creation paths (REST and
admin form) via AvailabilitySlot::normalizeDateTime(): canonical and
datetime-local forms normalise to Y-m-d H:i:s, garbage and end <= start
are rejected (REST 400) instead of reaching the DATETIME column or
throwing inside the weekly-series date arithmetic. Fixes#42.
composer test (204 tests, 594 assertions), PHPStan L6, and PHPCS all green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Availability (#2):
- us_availability gains offering_id, duration_minutes (default 60), and
recurrence_group; AvailabilitySlot carries the new fields.
- AvailabilityRepository::createWeeklySeries() generates N weekly rows
sharing a recurrence_group; findAvailable() filters by offering and
duration. Date math uses DateTimeImmutable::modify() (the no-debug CI
regex `dd\(` matches `->add(`).
- REST GET filters by offering_id/duration_minutes; POST accepts
duration_minutes, offering_id, recurrence (single|weekly) + weeks.
- Admin form adds duration, an offering picker, and one-off/weekly options
(OfferingRepository wired into AvailabilityController).
- booking.js renders an agenda calendar (slots grouped by day, with
duration). The richer booking UX lands with the booking-flow work.
Offering price in dollars:
- Switch us_offerings.price_cents (INT) to price DECIMAL(10,2); Offering
uses float $price. Admin form and REST take dollars.
- Fix a pre-existing misalignment in the Offering insert/update $wpdb
format arrays (billing_mode/capacity/is_active were mapped to the wrong
specifiers, which would corrupt values) via a single COLUMN_FORMATS list.
Also bump PHPStan to --memory-limit=1G in the lint script; 128M now
crashes analysis as the codebase has grown.
Refs #2
Co-Authored-By: Claude Opus 4.8 <[email protected]>
All classes are now organised by domain (Availability, Booking, Auth).
Each domain package contains its value object, repository, admin controller,
REST endpoint, and any shortcode pages under a matching sub-namespace.
Cross-cutting wiring (Plugin, AdminMenu, RestRegistrar, ShortcodeRegistrar,
Schema) lives at src/ root. Tests mirror the domain structure.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>