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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>