Files
unsupervised-scheduler/src/Schema.php
T
KydoimosandClaude Opus 5 1847159e31 Fix five findings from a security assessment of the plugin
The assessment looked for three things: whether students can reach each
other's bookings, whether payment settings can be dodged, and whether the
plugin opens a way into the rest of the install. The student-isolation and
payment paths held up. These are what did not.

- The front-end login form told WordPress not to work out whether the site
  was secure, so on HTTPS every student's session cookie was issued without
  the Secure flag. wp_signon() only derives it from is_ssl() when the second
  argument is left at its default; an explicit false reads like "no
  preference" and is not.

- The update check took whatever download URL the release API returned and
  handed it to core, which unpacks it over the installed plugin. The package
  must now be https on git.unsupervised.ca exactly, compared on the parsed
  host so a lookalike name cannot pass.

- Uninstalling dropped 2 of 14 tables and left the Stripe secret and webhook
  signing key in wp_options. Removal is now a choice made in advance on
  Access -> Plugin removal: records are kept unless the owner opts in (with a
  typed confirmation), while credentials and the borrowed core registration
  settings go every time.

- Open registration switches on the site-wide users_can_register and makes
  Student the default role, arming any other signup form on the site to mint
  students who could book and be billed immediately. The pending state is now
  decided once, on user_register, rather than by whichever form created the
  account.

- Cancel and withdraw answered "not yours" differently from "does not exist",
  which let a signed-in student enumerate the studio's bookings. Both now
  give the same 404.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 11:55:53 -03:00

308 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace Unsupervised\Schedular;
class Schema {
/**
* Every table this plugin owns, unprefixed and in creation order.
*
* The statements in {@see tables()} spell their own names out, so this list is
* what anything that needs to *name* the tables without building them reads —
* {@see Uninstaller}, which drops them. Add a table below and add it here, or
* uninstalling will leave it behind.
*
* @var list<string>
*/
public const TABLES = [
'us_availability',
'us_lessons',
'us_offerings',
'us_questions',
'us_question_answers',
'us_policies',
'us_policy_versions',
'us_policy_acceptances',
'us_payments',
'us_credits',
'us_group_enrollments',
'us_invites',
'us_guardians',
'us_group_access',
];
/**
* Returns CREATE TABLE statements for dbDelta.
*
* @return list<string>
*/
public static function tables( string $prefix, string $charset ): array {
return [
"CREATE TABLE {$prefix}us_availability (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
instructor_id BIGINT UNSIGNED NOT NULL,
offering_id BIGINT UNSIGNED DEFAULT NULL,
start_dt DATETIME NOT NULL,
end_dt DATETIME NOT NULL,
duration_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 60,
is_booked TINYINT(1) NOT NULL DEFAULT 0,
recurrence_group BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY instructor_id (instructor_id),
KEY offering_id (offering_id),
KEY start_dt (start_dt),
KEY recurrence_group (recurrence_group)
) {$charset};",
"CREATE TABLE {$prefix}us_lessons (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
slot_id BIGINT UNSIGNED NOT NULL,
offering_id BIGINT UNSIGNED DEFAULT NULL,
student_id BIGINT UNSIGNED NOT NULL,
instructor_id BIGINT UNSIGNED NOT NULL,
recurrence VARCHAR(10) NOT NULL DEFAULT 'single',
series_id BIGINT UNSIGNED DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
payment_id BIGINT UNSIGNED DEFAULT NULL,
notes TEXT,
booked_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY slot_id (slot_id),
KEY offering_id (offering_id),
KEY student_id (student_id),
KEY instructor_id (instructor_id),
KEY series_id (series_id)
) {$charset};",
"CREATE TABLE {$prefix}us_offerings (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
instructor_id BIGINT UNSIGNED NOT NULL,
kind VARCHAR(20) NOT NULL,
title VARCHAR(191) NOT NULL,
description TEXT,
duration_minutes SMALLINT UNSIGNED DEFAULT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
billing_mode VARCHAR(20) NOT NULL DEFAULT 'one_time',
allow_weekly TINYINT(1) NOT NULL DEFAULT 0,
capacity SMALLINT UNSIGNED DEFAULT NULL,
term_start DATE DEFAULT NULL,
term_end DATE DEFAULT NULL,
class_time TIME DEFAULT NULL,
enrollment_deadline DATE DEFAULT NULL,
withdrawal_deadline DATE DEFAULT NULL,
schedule_note VARCHAR(191) DEFAULT NULL,
etransfer_email VARCHAR(191) DEFAULT NULL,
cancellation_cutoff_hours SMALLINT UNSIGNED DEFAULT NULL,
access_mode VARCHAR(20) NOT NULL DEFAULT 'public',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY instructor_id (instructor_id),
KEY kind (kind),
KEY is_active (is_active)
) {$charset};",
"CREATE TABLE {$prefix}us_questions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
offering_id BIGINT UNSIGNED DEFAULT NULL,
scope VARCHAR(20) NOT NULL DEFAULT 'offering',
label VARCHAR(255) NOT NULL,
field_type VARCHAR(20) NOT NULL DEFAULT 'text',
options TEXT,
audience VARCHAR(20) NOT NULL DEFAULT 'all',
is_required TINYINT(1) NOT NULL DEFAULT 0,
is_required_child TINYINT(1) NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY offering_id (offering_id),
KEY scope (scope),
KEY is_active (is_active)
) {$charset};",
"CREATE TABLE {$prefix}us_question_answers (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
question_id BIGINT UNSIGNED NOT NULL,
registration_type VARCHAR(20) NOT NULL,
registration_id BIGINT UNSIGNED NOT NULL,
student_id BIGINT UNSIGNED NOT NULL,
answer_value TEXT,
collected_via VARCHAR(20) DEFAULT NULL,
collected_note VARCHAR(191) DEFAULT NULL,
recorded_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY question_id (question_id),
KEY registration (registration_type, registration_id),
KEY student_id (student_id)
) {$charset};",
"CREATE TABLE {$prefix}us_policies (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(191) NOT NULL,
slug VARCHAR(191) NOT NULL,
current_version_id BIGINT UNSIGNED DEFAULT NULL,
acceptance_scope VARCHAR(20) NOT NULL DEFAULT 'booking',
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY slug (slug),
KEY acceptance_scope (acceptance_scope)
) {$charset};",
"CREATE TABLE {$prefix}us_policy_versions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
policy_id BIGINT UNSIGNED NOT NULL,
version_number INT NOT NULL DEFAULT 1,
body LONGTEXT,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
published_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY policy_id (policy_id),
KEY status (status)
) {$charset};",
"CREATE TABLE {$prefix}us_policy_acceptances (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
policy_version_id BIGINT UNSIGNED NOT NULL,
student_id BIGINT UNSIGNED NOT NULL,
accepted_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
registration_type VARCHAR(20) NOT NULL,
registration_id BIGINT UNSIGNED NOT NULL,
accepted_at DATETIME NOT NULL,
ip_address VARCHAR(45) DEFAULT NULL,
collected_via VARCHAR(20) DEFAULT NULL,
collected_note VARCHAR(191) DEFAULT NULL,
recorded_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY policy_version_id (policy_version_id),
KEY student_id (student_id),
KEY accepted_by (accepted_by),
KEY registration (registration_type, registration_id)
) {$charset};",
"CREATE TABLE {$prefix}us_payments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
student_id BIGINT UNSIGNED NOT NULL,
payer_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
instructor_id BIGINT UNSIGNED NOT NULL,
registration_type VARCHAR(20) NOT NULL,
registration_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
method VARCHAR(20) NOT NULL DEFAULT 'etransfer',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
tax_rate DECIMAL(5,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
credit_applied DECIMAL(10,2) NOT NULL DEFAULT 0,
due_date DATE DEFAULT NULL,
period_key VARCHAR(20) DEFAULT NULL,
notice_batch VARCHAR(32) DEFAULT NULL,
etransfer_email VARCHAR(191) DEFAULT NULL,
stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
receipt_number VARCHAR(50) DEFAULT NULL,
receipt_sent_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL,
paid_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY student_id (student_id),
KEY payer_id (payer_id),
KEY instructor_id (instructor_id),
KEY registration (registration_type, registration_id),
KEY status (status)
) {$charset};",
"CREATE TABLE {$prefix}us_credits (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
student_id BIGINT UNSIGNED NOT NULL,
payer_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
remaining DECIMAL(10,2) NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
source_payment_id BIGINT UNSIGNED DEFAULT NULL,
source_lesson_id BIGINT UNSIGNED DEFAULT NULL,
reason VARCHAR(191) DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'available',
created_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY student_id (student_id),
KEY payer_id (payer_id),
KEY status (status),
KEY source_lesson_id (source_lesson_id)
) {$charset};",
"CREATE TABLE {$prefix}us_group_enrollments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
offering_id BIGINT UNSIGNED NOT NULL,
student_id BIGINT UNSIGNED NOT NULL,
instructor_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
payment_id BIGINT UNSIGNED DEFAULT NULL,
enrolled_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
enrolled_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY offering_id (offering_id),
KEY student_id (student_id),
KEY instructor_id (instructor_id),
KEY status (status)
) {$charset};",
"CREATE TABLE {$prefix}us_invites (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(191) NOT NULL,
token VARCHAR(64) NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'us_student',
kind VARCHAR(10) NOT NULL DEFAULT 'personal',
offering_id BIGINT UNSIGNED DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
invited_by BIGINT UNSIGNED DEFAULT NULL,
accepted_user_id BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL,
accepted_at DATETIME DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY token (token),
KEY email (email),
KEY status (status)
) {$charset};",
// Links a parent/guardian account to a child who books through it. The
// child is a real (login-less) wp_users row, so student_id keeps meaning
// "a WordPress user" on every other table.
"CREATE TABLE {$prefix}us_guardians (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
guardian_id BIGINT UNSIGNED NOT NULL,
student_id BIGINT UNSIGNED NOT NULL,
relationship VARCHAR(50) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY guardian_student (guardian_id, student_id),
KEY guardian_id (guardian_id),
KEY student_id (student_id)
) {$charset};",
"CREATE TABLE {$prefix}us_group_access (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
offering_id BIGINT UNSIGNED NOT NULL,
student_id BIGINT UNSIGNED DEFAULT NULL,
email VARCHAR(191) NOT NULL DEFAULT '',
invite_id BIGINT UNSIGNED DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'invited',
invited_by BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY offering_id (offering_id),
KEY student_id (student_id),
KEY email (email),
KEY status (status)
) {$charset};",
];
}
}