Commit Graph

6 Commits

Author SHA1 Message Date
thatguygriff 1d6ac46ba3 Upgrade PHPStan to 2.x and raise analysis level from 6 to 10
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 48s
CI / Tests (PHP 8.3) (pull_request) Successful in 52s
CI / Coding Standards (pull_request) Successful in 57s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / PHPStan (pull_request) Successful in 1m11s
CI / Build Plugin Zip (pull_request) Has been skipped
- 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 <noreply@anthropic.com>
2026-06-12 13:42:50 -03:00
thatguygriff f3f5c7801f Security fixes: CSV injection, policy body output, invite hashing, slot datetimes
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.1) (pull_request) Successful in 43s
CI / Tests (PHP 8.3) (pull_request) Successful in 49s
CI / Tests (PHP 8.2) (pull_request) Successful in 59s
CI / Coding Standards (pull_request) Successful in 1m11s
CI / PHPStan (pull_request) Successful in 1m20s
CI / Build Plugin Zip (pull_request) Has been skipped
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>
2026-06-10 16:36:26 -03:00
thatguygriff 061d09e034 Harden booking, offering exposure, payments, and invites
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / Coding Standards (pull_request) Successful in 55s
CI / PHPStan (pull_request) Successful in 1m7s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m41s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Build Plugin Zip (pull_request) Has been skipped
Security fixes from a pen-test review (issues #31–#37):

- #31 Booking no longer trusts a client-supplied offering_id: a slot-tied
  offering is authoritative and any offering used must belong to the slot's
  instructor, closing a free/misrouted-payment bypass.
- #34 Availability slot creation rejects an offering the instructor does not
  own (AvailabilityEndpoint now takes OfferingRepository).
- #32 Offering/question/policy listing endpoints now require book_lesson
  instead of being public (no anonymous consumer exists); Offering::toArray
  also omits etransfer_email from listings as defense-in-depth.
- #33 Slots are claimed atomically (UPDATE ... WHERE is_booked = 0) before a
  lesson is inserted, preventing a double-booking race.
- #35 A single weekly booking is capped (MAX_WEEKLY_OCCURRENCES) and only
  creates lessons for slots it actually claimed.
- #36 Stripe secret/webhook keys are write-only in the settings UI and a blank
  submit keeps the stored value; secrets are never echoed back into HTML.
- #37 Pending invites expire after 14 days (Invite::isAcceptable), enforced at
  registration and surfaced on the admin invites list.

Adds BookingEndpointTest plus Invite/Offering/AvailabilityRepository coverage
and minimal WP_REST_Request/WP_REST_Response stubs. composer test (200),
lint, and cs all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:08:22 -03:00
thatguygriff 6d163e5d0e Add lesson booking registration flow (offering, questions, policies)
CI / Coding Standards (pull_request) Successful in 1m51s
CI / PHPStan (pull_request) Successful in 2m17s
CI / Tests (PHP 8.1) (pull_request) Successful in 2m24s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Tests (PHP 8.3) (pull_request) Successful in 47s
CI / Build Plugin Zip (pull_request) Has been skipped
Implements #3: students register for a private lesson by picking a slot,
answering the offering's intake questions, and accepting booking-scoped
policies. Payment is a clean seam for #7 (lessons land pending; payment_id
null; instructor confirms via PATCH /bookings/{id}/status).

- Schema: us_lessons += offering_id, recurrence, series_id, payment_id.
- Lesson: new fields + recurrence constants.
- BookingRepository::insertSeries() builds a weekly series sharing a
  series_id; AvailabilityRepository::findUnbookedInGroup() reserves a group.
- RegistrationGate (src/Registration/): validate + record intake answers and
  booking-scoped policy acceptances. Reused by group enrolment (#4).
- BookingEndpoint::book(): offering_id, recurrence, answers,
  accepted_policy_version_ids; single or weekly; records answers/acceptances
  (type lesson).
- GET /policies?scope=booking filter.
- Front-end booking.js: slot -> questions + policies -> submit.
- Wiring: RegistrationGate built in Plugin, passed via RestRegistrar.
- Test-only WP_Error stub in tests/bootstrap.php for gate testing.

Refs #3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:25:30 -03:00
thatguygriff 9c900d6553 Add account registration with signup policy acceptance
CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / No Debug Code (pull_request) Successful in 2s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / Coding Standards (pull_request) Successful in 52s
CI / PHPStan (pull_request) Successful in 1m1s
CI / Tests (PHP 8.2) (pull_request) Successful in 48s
CI / Tests (PHP 8.3) (pull_request) Successful in 45s
Implements #16: invite-only student self-registration through a front-end
page, accepting signup-scoped policies at account creation.

Policy domain:
- us_policies.acceptance_scope (signup/booking/both); Policy::appliesTo();
  PolicyRepository::findForScope(); scope threaded through PolicyService,
  the REST create, the admin controller, and the Policies form.
- PolicyAcceptance::REG_ACCOUNT (registration_id = the new user's ID).

Auth:
- Invite value object + InviteRepository; us_invites table.
- RegistrationController + Invites admin page (manage_students): invite an
  email, share the registration link, revoke.
- RegistrationPage ([us_student_register] shortcode): validates the invite
  token, collects name/password, renders signup-scoped published policies
  with required acceptance, creates the us_student user, records account-type
  acceptances, marks the invite accepted, and logs the user in.
- RoleManager: manage_students cap added to STUDIO_ADMIN_CAPS.

Invite-only is implemented; the us_registration_mode self_approval path is a
documented future seam.

Docs: docs/features/account-registration.md; policies.md updated.
Tests: tests/Unit/Auth/ (Invite, InviteRepository) plus Policy scope
updates. composer test (104), cs, and PHPStan level 6 all pass.

Refs #16

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:39:39 -03:00
thatguygriff 6225e772f8 Add Policies domain (drafting, versioning, tracked acceptance)
CI / Coding Standards (pull_request) Successful in 1m0s
CI / PHPStan (pull_request) Successful in 1m4s
CI / Tests (PHP 8.1) (pull_request) Successful in 59s
CI / Tests (PHP 8.2) (pull_request) Successful in 56s
CI / Tests (PHP 8.3) (pull_request) Successful in 57s
CI / No Debug Code (pull_request) Successful in 3s
CI / Build Plugin Zip (pull_request) Has been skipped
Implements #6: studio admins draft, version, and publish policies; the
public registration gate reads the current published version of each, and
acceptance is recorded against the exact version so a new version must be
re-accepted at the next booking.

- src/Policy/: Policy, PolicyVersion, PolicyAcceptance value objects;
  PolicyRepository, PolicyVersionRepository, AcceptanceRepository;
  PolicyService (orchestrates create/add-draft/publish across the policies
  and versions tables); PolicyEndpoint (REST); PolicyController +
  templates/admin/policies.php (Policies admin menu, manage_policies)
- us_policies, us_policy_versions, us_policy_acceptances tables in Schema
- REST: public GET /policies (current published versions); manage_policies
  for create, add version, edit draft, and publish
- Wiring in Plugin, RestRegistrar, AdminMenu

AcceptanceRepository is built now and consumed by the booking/enrolment
gate in #3/#4.

Also bump PHPStan to --memory-limit=1G in the composer lint script; the
default 128M now crashes the analysis as the codebase has grown.

Tests: tests/Unit/Policy/ (value objects, repositories, service).
composer test (90 total), cs, and PHPStan level 6 all pass.

Refs #6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:00:54 -03:00