# Technical documentation — sawis_staging

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:silicon_bit_bucket/sawis_staging.git`.

Default branch `master` is `9d06011` (2025-06-20). The application described here is the tree on `qa_staging_2020` at `5040b8b` (2026-09-21), with production differences called out from `prod_staging_2020` at `39368c5` (2026-09-14).

Passwords are stored in the server PHP files. They are not copied into this document.

## Architecture

Custom PHP pages and cron scripts on top of an in-repo framework named Nemo. There is no Composer project, no front controller, and no router. Each URL is a PHP file.

`system.php` is the bootstrap the cron scripts include. It loads every `system.server.*.php` file, starts a session, connects to the S3 MySQL profile, copies `sysSettings` into `$SystemSettings`, then switches the connection to the staging profile.

Global request binding is the block at the top of `_framework/system.functions.inc.php`. A non-empty `$_POST` is copied into `$GLOBALS` and `$_GET` is set to null. Otherwise GET and cookies are copied. Including that file is what makes `$Action` exist. `sync.ui.php` does not include `system.php` before it switches on `$Action`.

Persistence is MySQL through `mysqli`. The wrapper is `NemoDatabase` in `_framework/_nemo.database2.cls.php`.

## Application structure

```text
index.php                     public stub
system.php                    bootstrap
system.server.PROD.php        profiles for host 10.0.0.62
system.server.QA.php          profiles for host 10.0.0.108
system.server.DEV.php         profiles for localhost, lamp, debian, 10.0.0.145, 192.168.100.6, localhost:8888
project.functions.inc.php     sync helpers
cron.sync.php                 sync engine
cron.sync.new.php             sync engine included by the UI
cron.mapper.php               CSV import
cron.blendmaster.php          BlendMaster SQL fragment
cron.daily.php                empty action switch
sync.ui.php                   operator page
query.php                     SQL console
includes/query.cls.php
message.php
deploy.php
phpinfo.php
layouts/                      header, basic layout, menu layout
css/                          menu, query, colourless, jquery-ui 1.8.6 smoothness
images/                       logos, including siliconLogo.png and Xprocure.png
_framework/                   Nemo classes and system.functions.inc.php
zindex.html                   Debian Apache default page
prettify.html
```

On `master` only: `bitbucket-pipelines.yml`, `sonar-project.properties`, `cron.sync.20180926.php`, `cron.sync.maanie.php`, `cron.sync.pj.php`, `project.functions.inc.20181101.php`. Those cron and function copies are not on the QA tip.

`master` is missing `system.server.QA.php` and `system.server.DEV.php` even though `system.php` includes them.

## Languages and frameworks

| Item | Evidence |
|---|---|
| PHP | All application files. Short open tags are not required; files use `<?php`. QA `system.php` uses `??` and quoted `$_SERVER` keys |
| PHP version | Commit subjects "php 8.2 upgrade" (2025-12-15) and "upgrade 8.4" (2026-03-09). No runtime pin in git |
| MySQL | `mysqli_*`. Identifier style is `tbl`, `stg`, `log`, `sys`, `vie` |
| JavaScript | Inline in the header and in `sync.ui.php`. jQuery 1.12.4 from Google. Local `js/` files are commented out and are not in git |
| CSS | Custom styles plus jQuery UI 1.8.6 "smoothness" |
| Nemo | Classes `NemoBasic`, `Nemo`, `NemoDatabase`, `NemoEmail`, `NemoMenu`, `NemoList`, `NemoDetails`, `NemoTabs`, `NemoWizzard`, `NemoReport`, `NemoControl`, `FAQ` |
| HTML | `layouts/header.incl.php` emits an XHTML 1.0 Strict doctype. `deploy.php` is HTML5 |

`NemoBasic` reads `layouts/` and `modules/` into arrays. `modules/` is not in git. `Display()` includes the layout file whose name is the `Layout` property. The default layout is `layout.basic.incl.php`. `message.php` asks for `layout.menu.incl.php`.

`gpc_extract` on the QA tip always calls `stripslashes` and does not take `$target` by reference. `qsQuery` still calls `get_magic_quotes_gpc()`, removed in PHP 8.

## Databases

Profiles are PHP objects. The array key is the HTTP `SERVER_NAME` the process must see. Fields used by `switchBD` are `hostname`, `username`, `password`, `database`, and `sessionpath` on the staging profile.

### Production host key `10.0.0.62`

From `system.server.PROD.php`. QA tip unless noted.

| Profile variable | Environment label | MySQL host | Database | Username |
|---|---|---|---|---|
| `$DATABASE_SETTINGS` | PROD | `10.0.0.62` (comment also says `SRV22-015`) | `sawis_staging` | `silicon` on the QA tip. `root` on `master` |
| `$DATABASE_SETTINGS_S3` | PROD | `10.0.0.61` (comment: ws ip `10.0.0.60`) | `sawis` (commented alternative `sawis_test`) | `root` |
| `$DATABASE_SETTINGS_API` | PROD | `10.0.0.173` (comment `Srv22-051`) | `sawis_api` | `silicon` on the QA tip. `root` on `master` |
| `$DATABASE_SETTINGS_IPW` | PROD | QA tip: `10.0.0.105`, database `ipw_live`, user `siliconoverdrive` | Production tip: `10.0.0.155`, database `ipw_staging_20260823`, user `sawis_readon` | See previous column |

Session path for this host: `DOCUMENT_ROOT/SESSION`.

On `master`, the file assigns `$_SERVER[SERVER_NAME] = "10.0.0.62"`, so every request is forced onto this key. The QA tip comments that assignment out.

### QA host key `10.0.0.108`

File: `system.server.QA.php`. Not on `master`.

| Profile | MySQL host | Database | Username |
|---|---|---|---|
| Staging | `10.0.0.108` (comment `SRV22-052`) | `sawis_staging` | `root` |
| S3 | `10.0.0.120` (comment also mentions `10.0.0.149`) | `sawis` | `root` |
| API | `10.0.0.108` | `sawis_api` | `root` |
| IPW | `10.0.0.105` | `ipw_live` | `siliconoverdrive` |

A comment says "n/a - do not push QA API". The API profile is still defined. Session path: `DOCUMENT_ROOT/sawis_staging/SESSION/`.

### DEV file

`system.server.DEV.php` defines several keys. Later assignments overwrite earlier ones for the same key. After the whole file runs, the effective `localhost` staging profile points at host `10.0.0.108`, database `sawis_staging`, user `root`. The effective `localhost` S3 profile points at `10.0.0.120`, database `sawis`, user `test_dev`, and sets `vendorkey` `SAWIS3`. The effective `localhost` IPW profile points at `10.0.0.105` / `ipw_live`.

Other keys that remain:

| Key | Staging database | Notes |
|---|---|---|
| `10.0.0.145` | `sawis_staging` on `10.0.0.145` | S3 database `sawis` on the same host. Comment at the top of the file says "DEV - 10.0.0.145 sawis_online" |
| `lamp` | `sawis_staging` on localhost | S3 database `sawis_dev`. Also `$DATABASE_SETTINGS_PROD` database `sawis_prod`. IPW database `ipw_live` on localhost |
| `debian` | `sawis_staging` | Environment label `DEV-DEBIAN`. S3 database `sawis_dev` |
| `192.168.100.6` | `sawis_staging` on localhost | S3 database `sawis_dev`. API url property `192.168.100.6/sawis_api_v1`. `vendorkey` `SAWIS3`. Session path under `/sawis/SESSION/` |
| `localhost:8888` | Staging and API on `10.0.0.108` | S3 on `10.0.0.120` as user `test_dev`, `vendorkey` `SAWIS3` |

DEV overwrites QA when both files are included and the host key exists in both. Include order in `system.php` is PROD, then QA, then DEV.

### Tables the code names

Not a schema dump. These names are in SQL strings:

- `sysSettings` — settings, including `blnEnableSync`, `dtLastSyncDate`, `SMTP Send To`, `SMTP Send As`
- `sysSyncLog` — sync log
- `sysSyncProcess` — per-day processed flag
- `sysSyncMapping` — mapper column map
- `sysUser`, `sysLogin` — used by lockout SQL
- `stg<Type>`, `tbl<Type>`, `log<Type>` — one family per entity type in `features.md`
- `vieWineType_EN`, `vieInspector` — views referenced by the sync SQL
- `tblEmailTemplate` — `GetTemplate` in the email class
- Comment on the production IPW change names `sawis_compliance`

There is no migration tool in the repository. How tables are created is **Unknown / Requires Confirmation**.

## APIs

This repository is not an HTTP API. The "API" in the code is the MySQL database `sawis_api`.

`SendSMS` POSTs `application/x-www-form-urlencoded` to `http://www.mymobileapi.com/api5/http5.aspx` with fields `Type=sendparam`, `Username`, `Password`, `live`, `numto`, and `data1`. No sync script calls it.

`NemoEmail::Send` sends HTML with PHP `mail()`. It does not speak SMTP itself. From-address and recipient for the sync log come from `sysSettings`.

## External integrations

| System | How |
|---|---|
| SAWIS public site | Text link only (`www.sawis.co.za`) |
| S3 / API / IPW MySQL | `switchBD` during sync |
| Mapper drop folder | `DATA/IN` relative to the process working directory. Not in git |
| Bitbucket | Git remote `silicon_bit_bucket/sawis_staging`. `deploy.php` fetches a branch |
| SonarQube | `master` pipeline only |
| Google CDN | jQuery 1.12.4 |
| MyMobileAPI | `SendSMS`, unused by sync |

## Infrastructure and deployment

`deploy.php` is the only deployment mechanism in git. It `shell_exec`s:

```text
cd /var/www/html/sawis_staging/
git fetch origin <branch>
git reset --hard origin/<branch>
```

`<branch>` is `qa_staging_2020`, `dev_staging_2020`, or `prod_staging_2020` depending on which copy of the file was deployed. `master`'s copy has no `cd` and fetches `dev_staging_2020`. The document title is `SAWIS Upload QA` on all of them.

The path `/var/www/html/sawis_staging/` is the only document root the deploy script names. The web server, PHP-FPM pool, and TLS setup are not in the repository. `zindex.html` is the Debian Apache "It works" page, which suggests Apache on Debian was used at least once. Provenance: **Inferred** from that file's presence.

`testDeploy.txt`, `testDeploy - Copy.txt`, and `test_deploy.txt` are small marker files. They are not a deploy pipeline.

No Docker file, no `appspec.yml`, no CI on the staging branches.

### Bitbucket pipeline (`master` only)

- Clone depth: full
- Step name: SonarQube analysis
- Pipe: `sonarsource/sonarqube-scan:3.0.2`
- Variables: `SONAR_HOST_URL`, `SONAR_TOKEN`, `EXTRA_ARGS=-Dsonar.scanner.forceEmbeddedJava=true`
- Docker service memory: 3072 MB
- Cache: `~/.sonar`
- Runs for branch pattern `{master}` and for every pull request
- Project key: `sawis_staging`

The pipe does not deploy the application.

## Environment configuration

There is no `.env`. Configuration is:

1. Which `system.server.*.php` key matches `SERVER_NAME`.
2. Rows in S3 `sysSettings`, loaded on each request that includes `system.php`.
3. On the production tip only, environment variable `SAWIS_SERVER_NAME` when the script is CLI. If it is unset, CLI uses `10.0.0.62`.
4. On the production tip only, timezone `Africa/Johannesburg`.

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

Session garbage-collection lifetime is 7200 seconds. Save path comes from the staging profile.

`cron.sync.php` sets `memory_limit` to `-1`.

## Authentication

No login form is implemented in this tree.

| Mechanism | Where | What it actually checks |
|---|---|---|
| IP substring | `query.php` | `192.168.100` or `10.212.134` inside `REMOTE_ADDR` |
| Session user | `message.php`, cron user name | Read if present. Not created here |
| `CheckSecurityLock` | `system.functions.inc.php` | Reads `sysLogin`. Does not save the lock |
| `password_verify` | `checkChangePassword` | Unused by the sync pages |
| Deploy | `deploy.php` | Nothing |
| `phpinfo.php` | Root | Nothing |
| Database accounts | `system.server.*.php` | MySQL users, several of them `root` |

`Login()` would set cookies `celusername` and `celpassword`. Nothing in the sync flow calls it.

## Background work

The cron files are ordinary PHP scripts. They are not queue workers. No beanstalk, database queue, or supervisor config is in git.

| Script | When it does work |
|---|---|
| `cron.sync.php` | Included or requested directly. Stops if sync is disabled and no action/process was posted |
| `cron.sync.new.php` | Included by several `sync.ui.php` actions, including Execute |
| `cron.mapper.php` | Stage `STAGE 1` imports the cultivar file. Any other action hits the default and truncates `sysSyncLog` |
| `cron.daily.php` | Default branch of the switch is empty |
| `cron.blendmaster.php` | SQL text. It is not wrapped as a scheduled job in git |

Who schedules them is **Unknown / Requires Confirmation**.

`cron.sync.php` may run `SET GLOBAL sql_mode=''`.

## Storage

Entity data is in MySQL. Mapper files are expected on the local disk under `DATA/`. Session files are under the `sessionpath` directory. Email attachments are a feature of `NemoEmail` (file list on the object). The sync log email does not add attachments.

Nothing in the repository uses object storage. The profile name S3 is a MySQL connection whose database is `sawis`.

## Monitoring and logging

| Channel | Behaviour |
|---|---|
| `sysSyncLog` | Info, Warning, Error, and the code also filters a level named `Dev` out of the email |
| `emailSyncLog` | Mails one hard-coded day of that table |
| PHP `display_errors` | Errors render in the response |
| `phpinfo.php` | Full PHP information page |
| SonarQube | Static scan of `master` and of pull requests. Staging-branch commits do not carry the pipeline file |
| Deploy page | Echoes git stdout |

No application metrics, health URL, or log shipper is configured in git.

## Security notes

These are properties of the tree, listed so they can be reviewed. Secret values are omitted on purpose.

- Database passwords are committed in `system.server.PROD.php`, `system.server.QA.php`, and `system.server.DEV.php`. The production-tip IPW block also places a password in a comment.
- Several profiles use MySQL user `root`.
- `deploy.php` runs `git reset --hard` through `shell_exec` with no application login.
- `query.php` executes operator SQL, including a mode that disables foreign-key checks, after a substring IP check.
- `phpinfo.php` is ungated.
- `display_errors` is on.
- `Login()` would put a password in a cookie. It is unused by the sync scripts and still present.
- The email class contains a test block addressed to a personal mailbox when `$Action` is `Test`, including a sample password string in that block.
- `gpc_extract` copies request fields into globals. `query.php` and `sync.ui.php` then use those variables in SQL with quoting that is inconsistent (`Set Last Sync Date` concatenates the posted timestamp after a format check).
- Sync-log error text stores MySQL errors, which can include parts of the statement.

## Development and testing

- No README, no contribution guide, no `.editorconfig`.
- `.gitignore` is the Bitbucket template (node_modules, class files, logs, IDE, media). It does not ignore `system.server.*.php`.
- No PHPUnit, no Composer, no fixture database.
- Manual verification visible in history is commit subjects such as "testing if deploy works", "debug", and "test".
- `qsQuery` and `get_magic_quotes_gpc` are incompatible with an unmodified PHP 8 runtime.
- QA `doQuery` does not accept the third argument that `doQueryUpdateStg` passes. Production `doQuery` does, and it catches `mysqli_sql_exception`.

## Code history that explains the layout

| Commit | Date | Fact |
|---|---|---|
| `3a49f76` | 2020-10-26 | Initial commit, Gareth Bowers |
| `a5bcf71` | 2020-10-26 | "ini sawis_staging repo", JacquesOverdrive |
| `63f812f` | 2021-07-29 | Hotfix branch tip. Lab analysis API status `ST` |
| `7a14067`, `712a45e` | 2023-05-18 | `system.server.PROD.php` edited on `master` by Jacques |
| `f7f9d66`, `f5fee1c`, `9d06011` | 2025-06-20 | Pipeline and Sonar properties on `master`, Jaco Brits |
| `39368c5` | 2026-09-14 | Production branch tip |
| `5040b8b` | 2026-09-21 | QA branch tip |

Author names above are the git author fields.
