# Technical documentation — doh_sims

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/doh_sims.git`, `main` at `5747dca4e0a45cb6edc95d5ec2cf262ad9278d19` (2026-10-07).

Shallow clone. Vendor checkout on Windows dropped some `vendor/google/apiclient-services` files because the path exceeded `MAX_PATH`. Those misses are Google client class files, not application modules. Do not treat a missing vendor file on that machine as a product deletion.

## Architecture

Gibbon is a PHP application. `index.php` is the front controller. `gibbon.php` builds the container, session, absolute URL, module autoload, and CSRF check. Pages are module scripts addressed as `index.php?q=/modules/<Module>/<script>.php`. Apache rewrites non-file requests to `index.php` with `RewriteBase /gibbon/` (`.htaccess`).

```text
.htaccess (RewriteBase /gibbon/)
  → index.php
      require gibbon.php
      require modules/DOHCustomFields/moduleFunctions.php
      require modules/Messenger/moduleFunctions.php
      render shell, menu, welcome data (includes paymentStatusEnabled)
  → modules/<Name>/*.php
  → src/  (PSR-4 Gibbon\)
  → MySQL
```

Overdrive code sits in extra module directories and in `src/Domain/ReEnrolment/`, which is not a stock Gibbon domain name. **Inferred** from the namespace and the Registrations bootstrap, which references `Gibbon\Domain\ReEnrolment\ProgressionEligibilityService`.

MultiCampus and Registrations are written to be "upgrade-safe" by adding one `require` into core. In this commit those requires are missing. See [Integration touch points](#integration-touch-points).

## Application structure

| Path | Role |
|---|---|
| `index.php` | Front controller and home page |
| `gibbon.php` | Bootstrap, CSRF, nonce |
| `version.php` | `29.0.02` and `$systemRequirements` |
| `CHANGEDB.php` | Core SQL from v20.0.00 upward. Header says v19 and earlier were removed |
| `gibbon.sql`, `gibbon_demo.sql` | Install and demo databases. Demo data includes people; do not treat it as DOH data |
| `src/` | Framework: Auth, Comms, Contracts, Data, Database, Domain, Forms, Http, Install, Security, Services, Session, Support, Tables, UI, View |
| `modules/` | One directory per Gibbon module, stock and custom |
| `themes/`, `resources/`, `i18n/`, `uploads/`, `cli/`, `installer/` | Theme, static assets, translations, uploads, CLI, installer |
| `vendor/` | Composer dependencies, committed |
| `composer.json` | Package name `gibbonedu/core`, GPL-3.0, PHP platform 7.4 |
| `azure-pipelines.yml` | Build and SSH deploy |
| `sonar-project.properties` | Sonar key only |
| `config.php` | Not in git. Produced by install |

Root entry scripts besides `index.php` include `login.php`, `logout.php`, `passwordReset.php`, `publicRegistration.php`, `preferences.php`, `notifications.php`, `export.php`, `report.php`, `fullscreen.php`, `keepAlive.php`, `roleSwitcherProcess.php`, `yearSwitcherProcess.php`.

## Languages and frameworks

| Item | Evidence |
|---|---|
| PHP `^7.4` | `composer.json` `require` and `config.platform.php` = `7.4` |
| MySQL ≥ 5.7 | `version.php` |
| Apache `mod_rewrite` | `version.php`, `.htaccess` |
| Twig 3 | `composer.json` |
| Slim 4 | `composer.json` (present; route map not traced) |
| League Container 3 | Service container |
| Aura SQL Query, Aura Auth | Database and auth libraries |
| jQuery UI 1.14.1 | CHANGELOG v29.0.00 |
| TCPDF and mPDF | Invoices and reports |
| PhpSpreadsheet | CHE and other Excel exports |
| PHPMailer 6 | Mail |
| spomky-labs/otphp and endroid/qr-code | TOTP MFA |
| Omnipay PayPal and Stripe | Payments |
| league/oauth2-client, microsoft/microsoft-graph, google/apiclient | Dependencies. Usage not traced in this pass |
| Monolog, Symfony YAML, Faker, ParseCSV, eluceo/ical, matthewbdaly/sms-client | Dependencies |

PHP extensions required by `version.php`: gettext, mbstring, curl, zip, xml, gd, intl. Composer also requires PDO, curl, intl, mbstring, gettext.

**Conflict:** `modules/MultiCampus/README.md` says PHP 8.0 or higher. `modules/Payment Status/README.md` says PHP 7.3.0. Core says 7.4. The pipeline passes `--ignore-platform-req=php`.

## Module versions in this tree

| Module | version.php | manifest.php if different |
|---|---|---|
| Core | 29.0.02 (`version.php`) | — |
| DOHCustomFields | 0.0.14 | README still says 0.0.06 |
| Registrations | 1.5.101 | — |
| MultiCampus | 2.1.00 | README title 2.0.0; core requirement in version.php is `v29.0.00` |
| CHEReports | 1.0.63 | `coreVersion` in that file is `28.0.00` |
| InvoiceCustomizer | 1.4.4 | — |
| Payment Status | 1.0.02 | README 1.0.00; `coreVersion` `28.0.00` |
| LMSSync | 1.5.9 | — |
| Moodle | 1.1.02 | — |
| Moodle Cohorts | 1.1.04 | — |
| Advanced Reports | 1.3.00 | **1.5.01** |

## Databases

Install creates the Gibbon schema (`gibbon.sql`) plus module tables when each module is installed.

Custom or extension tables named in docs and manifests:

| Table | Owner |
|---|---|
| `gibbonMfa` | Core v29.0.01 |
| `gibbonPerson.studentID` | DOHCustomFields, nullable unique |
| `gibbonReEnrolmentIntent` | Registrations |
| `gibbonStudentWILHours` | Registrations |
| Credit / pass / core columns on course, class, and year group | Registrations. Exact column names differ between the README (`theoretical_credits` on the class) and the service (`gibbonCourse.credits`, `gibbonCourse.is_core`, `gibbonYearGroup.minimum_credits`, `gibbonYearGroup.max_core_carried`) |
| `gibbonCampusRole`, `gibbonCampusPerson`, `gibbonCampusPermission` | MultiCampus |
| `gibbonCHEReportCode`, `gibbonCHEReportProviderByHouse` | CHEReports |
| `gibbonCHEReportCampusYearGroupQualification` | Named in the file 63 doc |
| `gibbonProformaInvoiceApplication` | Invoice Customizer |
| `gibbonApplicationPaymentTracking` | Payment Status README. Created outside that module's own manifest (the README says the table must already exist) |
| `gibbonAdmissionsAccount` | Admissions. Required by Payment Status |

Module SQL is applied through Gibbon's module installer (`manifest.php` / `CHANGEDB.php`), not through a separate migration runner.

`gibbon_demo.sql` is upstream demo data. It is not a DOH dataset.

## Auth and session

- Login, logout, password reset, and public registration are root PHP scripts.
- Session object comes from the container in `index.php`.
- MFA: table `gibbonMfa`, AES-256-GCM, optional `$mfaEncryptionKey` in `config.php`, recovery codes hashed (CHANGELOG). Library: otphp. QR: endroid/qr-code.
- Impersonation: `modules/System Admin/impersonateUser.php` and `impersonateUserProcess.php`.
- Role switch and year switch: `roleSwitcherProcess.php`, `yearSwitcherProcess.php`.
- CSRF: `gibbon.php` checks token and nonce on `Process.php` posts with more than one field. Failures redirect with `return=error9` or `return=error10`.
- Campus session: `CampusSessionManager` in the MultiCampus module, only if the bootstrap runs.
- MultiCampus `campus_api.php` documents a limit of 60 requests per minute per session.

Password hashing details were not re-read. Upstream Gibbon stores password hashes on `gibbonPerson`. Demo SQL contains hashes. They are not reproduced in Silicon Ark.

## APIs

There is no single versioned REST API for the whole product. Surfaces found:

| Surface | Auth | Direction |
|---|---|---|
| `index.php?q=/modules/...` | Gibbon session, except scripts that opt out | Browser |
| `modules/DOHCustomFields/validateSAIdAjax.php`, `validateStudentNumberAjax.php`, `getNextStudentNumberAjax.php` | Documented as form POST from staff pages. Session requirement was not re-read | Browser → PHP |
| `modules/MultiCampus/campus_api.php` | Session, documented rate limit | Browser → PHP |
| `modules/LMSSync/actions/syncCron.php?token=` | Shared secret query parameter | Cron → PHP |
| Moodle REST `local_gibbon_sync_*` | Moodle token, optional `api_key` | Gibbon → Moodle |
| SQL views `moodleUser`, `moodleCohort`, `moodleCohortMember`, `moodleCohortCourse`, `moodleCohortCourseCredits`, `moodleYearGroupCredits` | Database credentials held by Moodle | Moodle reads Gibbon |

LMS Sync HTTP cron is unauthenticated apart from the token. The setup guide says to keep the token private.

## External integrations

| System | Purpose | Trigger | Failure |
|---|---|---|---|
| Moodle web services | Students and grades into Gibbon | Manual sync or cron | Log screen exists. Retry policy not read |
| Moodle database views | Cohorts and credits out of Gibbon | Moodle's own sync task, not in this repo | Unknown |
| SMTP | Proforma email with PDF | Application or registration submit when auto-generate is on | Changelog mentions an SSL verification bypass for SMTP in development. Where that bypass is toggled is **Requires confirmation** |
| PayPal, Stripe | Upstream Gibbon payments | Finance payment actions | Not traced |
| Azure DevOps | Deploy | Push to `main` or `releases/*`, and PRs to `main` | Pipeline `condition: succeeded()` on the deploy stage |
| SonarQube | Project key only | No scan step in `azure-pipelines.yml` | Unknown whether a scanner runs elsewhere |
| CHE | None. Files stay in the browser download | User export | N/A |

## Infrastructure and deployment

`azure-pipelines.yml`:

- Triggers: branches `main` and `releases/*`. Pull requests targeting `main`.
- Variable group: `deploy-secrets-dev` (comment lists qa and prod groups; they are not selected).
- Pool: `selfhosted-dev`.
- Build: `composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-req=php`.
- Tests: `vendor/bin/phpunit --testdox` only if that binary exists. A comment says to add TestSprite. No TestSprite call is present.
- Artifact: `gibbon.tar.gz` excluding `.git`, `tests`, `node_modules`.
- Deploy jobs, both `environment: gibbon-dev`:
  - `deployGibbon` using SSH endpoint `ssh-gibbon`.
  - `deploySIMSDev` using SSH endpoint `gibbon-dev-vm-connection`.
- Both upload to `/home/deploy/_incoming` and run `APP=gibbon ARTIFACT=... KEEP=5 bash /home/deploy/release.sh`.

`release.sh`, the VM, and the SSH service connections are not in the repository.

`bitbucket-pipelines.yml` is not in this commit. `sonar-project.properties` contains only:

```text
sonar.projectKey=overdrive_doh_sims_6cb51fc6-7a0e-4436-baca-4f4625d95b76
```

## Environment configuration

Not committed: `config.php` (database, absolute URL, optional MFA key). The installer writes it (`src/Install/Config.php`).

Module behaviour is stored in `gibbonSetting` scopes such as `Registrations`, `Payment Status`, `CHEReports`, `MultiCampus`, and Invoice Customizer settings. Values are data.

`.htaccess` assumes the app is mounted at `/gibbon/`.

Uploads used by custom modules include `uploads/Registrations/WIL_proof/`. The Payment Status README tells operators to empty `uploads/cache/templates` after editing the welcome template and to leave that folder at mode 755.

## Background work

No queue worker (Redis, database queue, or supervisor program) was found.

Scheduled work that is documented:

- LMS Sync cron over HTTP or CLI (`modules/LMSSync/CRON.md`).
- Gibbon's own core cron was not re-read. Upstream Gibbon uses CLI scripts under `cli/`.

## Security notes evidenced in the tree

- CSRF and nonce on process posts (`gibbon.php`).
- MFA table and lockout (changelog and README).
- `.htaccess` denies direct access to extensions including `sql`, `ini`, `log`, `yml`, `json`, `lock`.
- Impersonation is an administrator action with a primary-role check.
- Payment status lookup is email possession. The module README does not implement a password on that form.
- LMS cron token in the query string.
- Invoice Customizer changelog records an SMTP SSL verification bypass for development. Confirm it is off in production.
- Demo SQL and any committed secret would be a defect. This pass did not extract credentials. `config.php` is untracked by design. Do not copy `gibbon_demo.sql` password hashes into docs or chat.

## Development and testing

Composer scripts:

- `composer test` runs Codeception install + acceptance, then PHPUnit via Codeception, all under `tests/`.
- `composer test:phpstan` uses `phpstan.neon`.
- `composer test:codesniffer` runs PHPCS PSR-2 on `modules/Library/` only.

The `tests/` directory is **absent**. `phpstan.neon` was not confirmed in this pass. Azure runs PHPUnit only when `vendor/bin/phpunit` exists after a `--no-dev` install, which does not install PHPUnit. **Inferred:** the pipeline test step is a no-op on a production Composer install.

MultiCampus ships `modules/MultiCampus/tests/run.php` and a test plan. Registrations documents a browser hit to `test_eligibility.php` for staff with Manage Registration.

## Integration touch points

| Expected by | File that should include it | In git at `5747dca4` |
|---|---|---|
| DOHCustomFields script injection | `index.php` requires `moduleFunctions.php` | Yes, line 39 |
| Messenger functions | `index.php` | Yes |
| Payment Status flag | `index.php` template data | Yes, `paymentStatusEnabled` |
| Registrations bootstrap | README says `index.php`. Bootstrap comment says `gibbon.php` | Neither. Loaded from `modules/Admissions/applications_manage_acceptProcess.php` when the file exists |
| MultiCampus bootstrap | `index.php`, injected by `setup.php` and `scripts/post-upgrade.php` | No |

Hook scripts under `modules/MultiCampus/scripts/apply_*_hooks.php` edit other modules' PHP files on the server. A git checkout can therefore differ from a long-lived server that has had hooks applied. **Requires confirmation** against the deployed tree before assuming campus filters are active.

## Logging and monitoring

- PHP `error_log` in the Registrations bootstrap catch blocks.
- LMS Sync log UI.
- Invoice Customizer debug log mentioned in CHANGELOG 1.3.0.
- Gibbon System Admin **View Logs** is a core action added in `CHANGEDB.php` v20.
- No application performance monitor, health endpoint, or metrics exporter was identified in this pass.

## Build and local run

Documented upstream path: install Gibbon by creating the database and `config.php` (https://docs.gibbonedu.org). This repo does not add a Docker Compose file or a README section for DOH.

Practical requirements from the tree: PHP 7.4 with the extensions in `version.php`, MySQL 5.7+, Apache with `mod_rewrite`, `composer install`, then the web installer or an imported `gibbon.sql` plus module installs from System Admin.

`max_input_vars` must be at least 8000 or large forms (timetable, markbook) will truncate. **Documented** in `version.php`.
