# Technical documentation — CCID

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/ccid.git`, `master` at `1d3e6bf` (2026-04-16, Joshua G-D). 34 commits, first `2926fa9` on 2020-05-28. Only branch: `master`.

No README, OpenAPI spec, Docker file, or migration directory is in the repository. Credentials from `system.php` are intentionally omitted.

## Stack

| Piece | Evidence |
|---|---|
| PHP | Root scripts, mysqli, sessions. Comments dated 2022-08-29 say "PHP 8 Upgrades". Host version is **Unknown / Requires Confirmation** |
| UI | Server-rendered HTML from Nemo layouts, CSS under `css/`, jQuery-style calls in the page scripts and `js/scripts.js` |
| Database | mysqli. 2020-10-22 commit mentions MariaDB on Hetzner. Current engine version is **Unknown / Requires Confirmation** |
| Mail | `phpmailer/phpmailer` `^6.9` (`composer.json`). `NemoEmail::doMail` calls `PHPMailer::Send()` and does not set SMTP host, port, or auth |
| PDF / spreadsheet libraries | Vendored `mpdf`, `dompdf`, `PHPExcel`, `Excel`, `fpdi`, `html2pdf`. Not used by the contact, company, or search pages |
| CI | Bitbucket Pipelines, SonarQube scan only |
| Tests | None found |

`composer.json` requires only PHPMailer. The PDF and Excel trees are committed as plain directories, not Composer packages.

## Application structure

```text
ccid/
  index.php                 Nemo landing. Session required. Renders the back layout
  login.php                 NemoBasic. No session required
  resetPassword.php         NemoBasic
  ajaxfunctions.php         system.php only. No Nemo view check
  phpinfo.php               phpinfo()
  contact2024.php           Current contacts
  contact.php               Older tblContact screen
  company.php
  search.php
  search.result.php
  category.php
  group.php
  precinct.php
  positions.php
  user.php
  my.profile.php
  security.group.php
  settings.php
  message.php
  report.php                Inherited report shell
  system.php                Host switch, mysqli, sysSettings, session
  project.functions.inc.php Project helpers, including unused PDF and counters
  includes/                 Domain classes (one class per area)
  _framework/               Nemo (list, details, menu, database, password, email)
  layouts/                  header, back, menu, basic, faq, calendar, email
  css/ js/ images/
  documents/ profilepictures/
  vendor/                   Composer (PHPMailer)
  .htaccess                 HTTP to HTTPS
  htaccess_prod             Same rewrite rules
  bitbucket-pipelines.yml
  bitbucket-pipeline.yaml   Same pipeline content as the yml file
  sonar-project.properties
```

Nemo is not a router. Each URL is a separate script. `index.php` only constructs `Nemo` and calls `Display()`.

## Request path

1. The script includes a Nemo class, which includes `NemoBasic`, which includes `system.php`.
2. `system.php` maps `$_SERVER["SERVER_NAME"]` to hostname, database, session path, and protocol. It sets `session.gc_maxlifetime` to `7200`, sets `session.save_path`, and starts the session.
3. It opens mysqli. On failure it prints the settings object (which includes the password) and stops being useful. On success it loads every `sysSettings` row into `$SystemSettings`.
4. `Nemo::__construct` redirects to `login.php` when `$_SESSION["USER"]` is missing, then loads the group's URL permissions and blocks the script when `blnView` is not `1`.
5. The page script switches on `$_REQUEST["Action"]` and either renders a `NemoList` or a `NemoDetails` form.
6. `NemoDetails::AssimulateTable` builds controls from the live table. Detail fields therefore follow the database, not a form schema in Git.

`display_errors` is on. `error_reporting` is `E_ALL` without `E_STRICT`, `E_NOTICE`, and `E_WARNING`.

## Configuration

There is no `.env`. Configuration is split between `system.php` and `sysSettings`.

Host cases in `system.php`:

| `SERVER_NAME` | Role comment in code | Database name | Session path pattern |
|---|---|---|---|
| `www.ccid-database.co.za` | LIVE PROD | `ccidddmshv_db1` | `{DOCUMENT_ROOT}/SESSION/` |
| `ccid-database.co.za` | Rewritten to the www name before the switch | same | same |
| `dev.overdrive.co.za` | Comment says LIVE PROD; database is the QA name | `ccid_qa` | `{DOCUMENT_ROOT}/SESSION/` |
| `ccid.php8` | DEV REPO | `ccid_qa` | `{DOCUMENT_ROOT}/SESSION/` |
| `ccid.local` | DEV REPO | `ccid_qa` | `{DOCUMENT_ROOT}/SESSION/` |
| `localhost` | DEV REPO | `ccidddmshv_db1` | `{DOCUMENT_ROOT}/ccid/SESSION/` |
| `debian` | DEV REPO | `ccid` | `{DOCUMENT_ROOT}/REPO_CCID/webadmin/SESSION/` |
| `lamp` | DEV QA | `ccid` | `{DOCUMENT_ROOT}/ccid/webadmin/SESSION/` |

Production and the `ccid.local` / `dev.overdrive.co.za` cases use `sql28.jnb1.host-h.net` or `sql52.jnb1.host-h.net`. Usernames and passwords are in the same switch. They are not repeated here. They should be rotated because they are in Git history.

Protocol overrides: `ccid.local` forces HTTP. Production, `ccid.php8`, `dev.overdrive.co.za`, and `localhost` force HTTPS. A comment in `system.php` says the wrong scheme breaks login.

Settings read from `sysSettings` at startup, and referenced by name in code, include:

- `Brand`, `Title`
- `LogoutRedirect`
- `Rows per Page`
- `SMTP Send As`, `SMTP BCC`
- `PASSWORD_Enable_Policy`, `PASSWORD_Strength_Check`, `PASSWORD_Strength_Check_MinLength`, `PASSWORD_Strength_Check_Description`, `PASSWORD_MaxAge`, `PASSWORD_MaxHistory`, `PASSWORD_Restricted_Words`, `PASSWORD_Reset_Interval`
- `ProfileImageDirAdmin`
- `smsUsername`, `smsPassword`, `smsLive`
- `EnableMimeEncoding`
- `NumericChars` (used by `getJSRemoveAlpha`)
- Any key starting with `vie`, whose value is SQL executed by Create Views

The values are **Unknown / Requires Confirmation**.

## Data model (from SQL in the PHP)

No `CREATE TABLE` scripts are in the repository. Tables named by the application:

| Table | Role |
|---|---|
| `tblContact2024` | Current person. Fields used in SQL include `ContactID`, `strTitle`, `strName`, `strSurname`, `strEmail`, `strTel`, `strCell`, `txtNotes`, `blnActive`, `strLastUser`, `dtLastEdit` |
| `tblContact` | Older person model. Precinct is `refPrecinctID` on the contact. Still used by `contact.php` and by commented search SQL |
| `tblCompany` | Organisation. Fields used include `CompanyID`, `strCompany`, `refPrecinctID`, `strTel`, `strEmail`, `strEmergencyContactNo`, `strWebsite`, `txtPhysicalAddress`, `blnActive`, `strLastUser`, `dtLastEdit` |
| `tblCompanyContact` | Link. `refCompanyID`, `refContactID`, `strPosition` |
| `tblCompanyCategory` | `refCompanyID`, `refCategoryID` |
| `tblContactCategory` | Used by the old contact class and by `Contact::Delete`. The 2024 save writes company categories, not contact categories |
| `tblContactGroup` | `refContactID`, `refGroupID` |
| `tblCategory`, `tblCategoryType` | Category, optional `strSubCategory`, `strAlias`, `blnActive`, `intOrder` on the type |
| `tblGroup`, `tblGroupType` | Group, `strAlias`, `blnActive` |
| `tblPrecinct` | `strPrecinct`, `txtNotes`, `blnActive` |
| `tblPosition` | `strPosition`, `blnActive` |
| `tblField` | Searchable contact columns (`strField`, `strLabel`, `strAlias`, `intOrder`, `blnActive`) |
| `tblSearch`, `tblSearchField` | Saved search and its contact, category, and group criteria |
| `vieCompanyContact` | View. Selected columns in code: `ContactID`, `CompanyID`, plus whatever `tblField.strField` points at. Definition is not in Git |
| `sysUser` | `UserID`, `strUser`, `strEmail`, `refSecurityGroupID`, `blnActive`, `blnLogin` (written only in the unused lockout assignment), profile path, language setting |
| `sysSecurityGroup` | `strSecurityGroup`, `blnActive` |
| `sysSecurity` | Flags per group per menu item |
| `sysMenuLevel1`, `sysMenuLevel2` | Menu. Level 2 has `strEntity`, `strUrl`, `blnMenuItem`, `blnDivider` |
| `sysPassword` | `Type`, `UserID`, `salt`, `hash`, `dtPassword`, `dtLastEdit` |
| `sysLogin` | `strIP`, `strResult`, `strUsername`, `strPassword`, `strDateTime` |
| `sysSettings` | Key, value, comment |
| `sysSpam` | Written by `tblSpamInsert`. No CCID page calls it |
| `tblEmail`, `tblEmailTemplate` | Used by `NemoEmail` when a send is saved or a template is loaded |

Foreign keys appear only as comments (contact precinct, user security group, category type). Whether those constraints exist in the database is **Unknown / Requires Confirmation**.

## Auth

- Session key `USER`: `ID`, `USERNAME`, `EMAIL`, `SECURITYGROUPID`.
- Password salt: 32 bytes from `openssl_random_pseudo_bytes`, stored hex-encoded. Hash: SHA-256 of salt concatenated with the password. No password_hash / bcrypt / argon2.
- Reset token: the hash of a random password, placed in the query string `ResetKey`.
- View check compares the script file name with `sysMenuLevel2.strUrl`.
- `ajaxfunctions.php` actions (`SendResetEmail`, `setLanguage`, `SetSidebarDisplay`, `getCategoryTypeDetails`, `removeContact`, `getCategoryItemsXML`) are not behind `Nemo::iniSecurity`. `SendResetEmail` is usable without a session. The other actions trust the caller.
- `phpinfo.php` has no auth.

`windowLocation` echoes a JavaScript redirect and does not `exit`. Code after a "redirect" still runs unless the caller `die`s. Login does `die` after a successful redirect. `iniSecurity` does not.

## Mail

`NemoEmail::doMail`:

- Splits To, CC, and BCC on `;`.
- Supports `Name <email>` via a regex.
- HTML body, Calibri/Arial wrapper applied in `Send`.
- From address comes from the `from` header, set by callers to `SMTP Send As`.
- No SMTP configuration in code. PHPMailer therefore uses PHP `mail()`.
- Optional save to `tblEmail` with status `Sent` or `Error`. Password reset sets up a template named "Reset Password" with substitutions `DisplayName` and `Link`.

The template row is data. If it is missing, `LoadEmailTemplate` finds no row and the message has no template body. **Unknown / Requires Confirmation** whether that row exists in production.

## External HTTP

| Call | Used by CCID pages |
|---|---|
| `http://www.mymobileapi.com/api5/http5.aspx` POST `Type=sendparam` | No caller other than `SendSMS` |
| mailto `reception@capetownccid.org` | Login page only |

## Files and storage

- Sessions: filesystem path per host, under the document root `SESSION/` on production.
- Profile images: `ProfileImageDirAdmin` plus a file name `{UserID padded to 5}_{original name}`. Upload mode is `chmod` `0777` on the temp file before `move_uploaded_file`. Only when the user already has an id.
- `profilepictures/` and `documents/` exist in the tree. No contact-screen code writes into `documents/` in the files inspected.

## Deployment

`.htaccess` and `htaccess_prod` both contain:

```apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
```

The pipeline file (`bitbucket-pipelines.yml` and the duplicate `bitbucket-pipeline.yaml`) runs on branch `master`:

- Clone depth: full
- Docker memory: 3072 MB
- One step: `sonarsource/sonarqube-scan:3.0.2`
- Variables: `SONAR_HOST_URL`, `SONAR_TOKEN`
- Extra arg: `sonar.scanner.forceEmbeddedJava=true`
- Project key: `overdrive_ccid_37f93f0e-e455-4d3f-a532-73936b39392a`

There is no build, test, or deploy step. How files reach `www.ccid-database.co.za` is **Unknown / Requires Confirmation**.

`.gitignore` contains only `/index.html`.

## Process and quality tools

- No PHPUnit, Pest, or other test directory.
- No lint config in the repository root.
- SonarQube is the only automated check visible in Git.
- Error display is enabled in `system.php`, including on the production host case, because the `ini_set` is unconditional.

## Security behaviour worth a review

These are observations from the code, not a pentest.

- Database passwords are hardcoded in `system.php` and are printed with `print_rr` when mysqli fails.
- `sysLogin` has a password column, and `tblLoginInsert` assigns the request password to it.
- `phpinfo.php` is unauthenticated.
- Password reset confirms whether an email is registered.
- Password strength is enforced in JavaScript. The reset save path checks history and then stores the posted value.
- Several SQL fragments interpolate request values without `qs` (company id in the contact list when a company is passed, category type id in the contact form SQL, group and category alias checks, add-contact inserts). `NemoDatabase::qs` is used on many other writes.
- `SecurityGroup::Save` and `Settings::CreateViews` can change shared menu rows or run SQL stored in the database.
- `display_errors` is on.
- The five-failure lock does not persist `blnLogin`.

## Related Silicon Ark projects

No other Silicon Ark directory is referenced from this repository. The Nemo framework files are copied inside this repo (`_framework/`), not consumed as a package. A shared Nemo project, if one exists, was not linked from the CCID tree.

## Requires confirmation

- PHP version and web server (Apache is implied by `.htaccess` only).
- MariaDB or MySQL version, and whether the commented foreign keys exist.
- SQL text of `vieCompanyContact` and of the `vie*` settings.
- Contents of `sysMenuLevel1` / `sysMenuLevel2` / `sysSecurity`.
- Whether `contact.php`, `report.php`, and `phpinfo.php` are deployed and linked.
- SMTP: local `mail()` versus an external relay configured on the host outside this repo.
- Deploy mechanism and whether `htaccess_prod` is what the host actually uses.
- Rotation of the credentials stored in `system.php`.
