# Technical documentation — sawis (SAWIS Online)

Evidence date: 2026-10-04.

Source: `https://bitbucket.org/silicon_bit_bucket/sawis.git`, branch `dev_2020` at `069966f` (2026-09-14).

Passwords, database usernames, and vendor secrets from `system.server.*.php` are not copied. The files do contain them in plaintext.

Related system: `silicon_ark/sawis_api/` documents the API this application calls.

## Stack

| Item | Evidence |
|---|---|
| Language | PHP. Pages are scripts, not a router. Classes extend Nemo list/details types |
| Database | MySQL via `mysqli`. Database names `sawis`, `sawis_dev`, `sawis_training`, and a second database `sawis_api` |
| UI framework | In-repo Nemo under `_framework/` (list, details, menu, email, database, wizard, report, password, breadcrumbs, translations, tabs, control, CSS) |
| HTTP client | `curl` when the extension is loaded (`get_url` in the traceability worker and the same pattern on login). SSL peer verification is disabled on the traceability client |
| PDF | Composer `mpdf/mpdf` ^8.2. Also vendored `dompdf/` and `fpdi/` |
| Spreadsheets | Vendored `PHPExcel/` |
| Pagination | `Sauer_Pagination_Library/` |
| JavaScript | Files under `js/`, including jQuery File Upload 9.11.2, Morris, Cleave, html2pdf. Page behaviour is mostly server-rendered HTML plus page-specific scripts (`js/jsDuties.js`, `js/jsStockMovement.js`) |
| Frontend framework | None. No package.json at the root |
| PHP version | Upgrade branch `PHP_UPGRADE_8.2_2025_NEW_FEB25` is merged. A comment in `project.functions.inc.php` says “PHP 8.2:”. `composer.json` does not pin PHP. The version installed on each host is **Unknown** |
| Tests | No PHPUnit configuration for the application. `checkMemberAccessTest.php` is a manual script with commented ids. `test.php` and `test3.php` are ad-hoc. Pipeline does not run tests |
| Quality scan | SonarQube project key `SAWIS` |

About 4,085 files were in the shallow checkout, including `vendor/` and vendored libraries.

## Boot sequence

Almost every page includes the Nemo stack, which pulls in `system.php`.

`system.php`:

1. `display_errors = 1`. Reporting is `E_ALL` without strict, notice, warning, or deprecated.
2. Normalise host names (`sawisonline.co.za` → `www.sawisonline.co.za`, training www host → `training.sawisonline.co.za`, QA IP → `196.22.249.180:11443`).
3. `switch` on `SERVER_NAME` includes exactly one of PROD, QA, DEV, TRAINING. Unknown hosts include nothing. `system.server.LOCAL.php` is never included.
4. Build `BASE_URL`, `FULL_URL`, and `BASE_URL_API` from the API settings `url`. Localhost overrides `BASE_URL_API` to `https://10.0.0.108/sawis_api`.
5. Session lifetime 7200 seconds. `session.save_path` comes from the settings object (typically `DOCUMENT_ROOT/SESSION/`). `session_start()`.
6. Include framework functions, menu class, and `project.functions.inc.php`.
7. `mysqli_connect` to the application database. On failure, a critical message is set and the connection settings are not printed.
8. Load every `sysSettings` row into `$SystemSettings`.
9. If `blnSyncInProgress` is 1, sleep up to 60 seconds.
10. If `SYSTEM_UNDER_CONSTRUCTION` is 1, clear the session and redirect to `server.error.php?MID=uc`.
11. Pilot-group URL gate (see business rules).
12. If a user session exists, `Global_LogUserNavigation()`.

`ini_set` for upload and POST size is 10M. A root `php.ini` is also in git (a full PHP sample ini). Whether the host uses that file is **Unknown**.

## Environments

Last assignment wins inside `system.server.DEV.php`. Commented blocks are ignored.

| File | Host keys | App database host | App database | API URL |
|---|---|---|---|---|
| `system.server.PROD.php` | `www.sawisonline.co.za`, `10.0.0.60` | `10.0.0.61` | `sawis` | `api.sawisonline.co.za` (comment: host `10.0.0.173`, database `sawis_api`). Vendor key field is set; value omitted |
| `system.server.QA.php` | `qa.sawisonline.co.za`, `10.0.0.149`, `196.22.249.180`, `196.22.249.180:11443` | `srv22-050` | `sawis` | `10.0.0.108/sawis_api` |
| `system.server.TRAINING.php` | `training.sawisonline.co.za` | `srv22-050` | `sawis_training` | `api.sawisonline.co.za` (production API host). **Requires Confirmation** |
| `system.server.DEV.php` | `localhost` (final block) | `10.0.0.120` | `sawis` | `10.0.0.108/sawis_api`. Session path suffix `/sawisonline/SESSION/` |
| `system.server.DEV.php` | `localhost:8888` | `10.0.0.120` | `sawis` | `localhost:8888/sawis_api` in the settings object, but `system.php` then forces `https://10.0.0.108/sawis_api` |
| `system.server.DEV.php` | `10.0.0.145` | `10.0.0.120` | `sawis` | `10.0.0.108/sawis_api` |
| `system.server.DEV.php` | `lamp`, `192.168.100.6` | `localhost` | `sawis_dev` | `lamp/sawis_api_v1` or `192.168.100.6/sawis_api_v1` |
| `system.server.LOCAL.php` | `localhost`, `lamp`, `192.168.100.6` | various | `sawis_dev` | Not loaded by `system.php` |

QA and training share the server name `srv22-050` and use different database names. Production application data is on `10.0.0.61`, not on the web host key `10.0.0.60`.

`external.db.connect.php` repeats the same host switch for callers that must not include the Nemo framework (the file comment says including the framework breaks that path). It is the connection helper for the traceability worker.

## Repository layout

```text
index.php                     Login
home.php                      Vision page and member switch
system.php                    Boot
system.server.*.php           Host maps (secrets)
project.functions.inc.php     Shared domain helpers (large)
project.functions.inc.pj.php  Older helper copy
_framework/                   Nemo
includes/*.cls.php            One class per domain page
*.php                         One page per domain (my.* = member twin)
ajaxfunctions.php             Large AJAX switch
ajax.BlendSandbox.php
ajax.TankRegister.php
_cron.daily.php
deploy.php                    QA git reset
get.and.store.wsb.number.traceability.data.php
start.wsb.number.traceability.data.retrieval.process.php
check.wsb.number.traceability.data.retrieval.status.php
layouts/                      Header and page layouts
css/ js/ images/
documents/                    Generated and uploaded files
SESSION/                      Session file directory placeholder
profilepictures/
composer.json                 mpdf only
bitbucket-pipelines.yml
sonar-project.properties
NewKnowledgeBase.md           Developer notes, 2025-01 to 2026-02
vendor/                       Composer install of mpdf
dompdf/ fpdi/ PHPExcel/ mpdf/ Sauer_Pagination_Library/
```

Page pattern: the root script handles `Action`, then instantiates the class in `includes/`. Lists use `NemoList`. Edit screens use `NemoDetails` and `AssimulateTable` against a MySQL table, so columns follow the database, not a schema file in git. There is no migration directory.

## Authentication

1. Application database: find `sysUser` + active `sysSecurityGroup`.
2. `switchBD($DATABASE_SETTINGS_API)` and read `apiVendor` by vendor key.
3. HTTP `call=authenticate` with the vendor password.
4. HTTP `call=loginUser` with the user’s email and password.
5. Session object `$_SESSION["USER"]` (id, username, email, security group, member id, company id, language, profile path).
6. `$_SESSION["USERADMIN"]` holds a hash login URL for Admin Dev and Sawis Admin.

Password storage and token tables are in `sawis_api` (`sysPassword` / `apiToken` — see that technical document). This app also calls `NemoPassword::getLastPassword("User", $userId)` while connected to the API database.

`switchBD` is in the framework functions. It replaces the global mysqli link.

## Authorisation

Two layers:

1. **Menu.** `sysMenuLevel1` / `sysMenuLevel2` joined to `sysSecurity` for the session security group. Flags `blnView`, `blnSave`, `blnDelete`, `blnSpecial`.
2. **Hard-coded group ids** in page scripts, especially `1` and `2` for cross-cellar access, plus the pilot list in `system.php`.

Entity checks use `checkMemberAccess` in `project.functions.inc.php` (a second copy exists in `project.functions.inc.pj.php`).

Multi-company membership is `sysUserCompanyGroup`, maintained at login and by `_cron.daily.php`.

## Domain data (tables named in the code that was read)

This is not a full schema. Tables appear in SQL in the files cited.

| Table | Used for |
|---|---|
| `sysUser`, `sysSecurityGroup`, `sysSecurity`, `sysUserCompanyGroup` | Users and permissions |
| `sysMenuLevel1`, `sysMenuLevel2` | Navigation |
| `sysSettings` | Runtime flags and SMTP keys |
| `sysLogin`, `sysIPBlock` | Login audit and IP block |
| `tblMember`, `tmpMember` | Members and registration drafts |
| `tblMemberType`, `tblMemberRelationship` | Types and relationships |
| `tblFarm`, `tblBlock`, `tblCultivar`, `tblHarvest`, `tblHarvestLoad` | Vineyard and harvest |
| `tblTank` | Tanks, including lock columns `blnLockTank`, `refLockTankUserMemberID`, `dtLockTank` |
| `tblBlendMaster` | Blend / WSB composition |
| `tblBottlingNotices` | Bottling WSB checks |
| `tblWineType` | Wine type codes |
| `tblProcessLogicRules` | Configurable process rules |
| `tblEmail`, `tblEmailTemplate` | Mail log and templates |
| `apiVendor` | Lives in the API database; read during login |

WSB identity in `tblBlendMaster`: `WSBID`, `CertificateTransactionCode`, `BlendNumber`, linked to `refWSID`.

## Integrations

### SAWIS API

Production base `https://api.sawisonline.co.za` (path is the settings `url` plus `/api.php`). Calls used here:

- `authenticate`
- `loginUser`
- `setPassword`

Query parameters include `VendorKey`, `call`, `Password`, `Username`, `Token`. See `silicon_ark/sawis_api/prd.md` for token lifetime, vendor keys, and HTTP 202 error text.

### Mapper

`get.and.store.wsb.number.traceability.data.php` GETs:

`{MapperAPIServerPath}/ice/default.asp?Category=SAWISONL&Service=WSBNUMQUERY3&CTWSB={6}&TRNTYP={3}&BLNNUM={2}`

The response is JSON. A `Message` property is treated as an error. cURL timeout is 10 seconds. `CURLOPT_SSL_VERIFYPEER` is false.

### Stellenbosch GIS

Browser `XMLHttpRequest` POST of `{"farm_id": "..."}` to `https://sungis10.stb.sun.ac.za/sawisvm/`. The SAWIS API CORS list (documented on `sawis_api`) includes that origin, which matches a 2019 comment there that CORS was added for GIS.

### Mail

`NemoEmail` loads a template by name, substitutes placeholders, and sends. From-address and BCC come from `sysSettings` (`SMTP Send As Sawis3`, `SMTP BCC`). A remote branch `feature/20260623-Russel-SendEmailViaSMTP` is not part of this tip.

### Cron

`_cron.daily.php` is a PHP page that impersonates user id `-1`. It is not a queue worker. No other scheduler file was found. Tank-unlock SQL is commented out.

WSB traceability is started as a PHP process with argv (`FullWSBNumber`, server name), not as a queue.

## File storage

`documents/` subfolders present in git:

- `BlendSandboxReports`
- `FAQ`
- `SAWIS_1_Reports`
- `SAWIS_6_Documents`
- `Signed_SAWIS_6_Documents`
- `TankCalibrationCert`
- `thumbnail`

Inspector audit notes say uploads go to `documents/Inspector_Audit_Documents/`. That folder was not in the directory listing. It may be created at runtime and gitignored, or the note may be ahead of the tree. **Requires Confirmation.**

Sessions are files under the configured `SESSION` path. `profilepictures/` holds user images. Upload cap in `system.php` is 10M. A 2017 comment on `document.cls.php` mentions `max_uploaded_size`.

## Security notes (observable, not a pentest)

- Database credentials are plaintext in `system.server.*.php`, and those files are in git.
- `deploy.php` runs `git fetch` and `git reset --hard origin/qa_2020` through `shell_exec` with no session check in the file. Title in the HTML: “SAWIS Upload QA”.
- Login passes the user password on the API query string (`loginUser&Password=`).
- Traceability cURL disables SSL peer verification.
- `display_errors` is on.
- Failed-login blocking is an insert; confirm the request path actually rejects `sysIPBlock` rows.
- `xphpinfo.php` is at the repository root. Whether it is deployed is **Unknown**.
- SonarQube exclusions include `member.registration.cls.php` (the registration wizard), `vendor`, `mpdf`, `PHPExcel`, `fpdi`, `dompdf`, `lib`, `tests`, `docs`, and non-PHP for the PHP step.

## CI

`bitbucket-pipelines.yml`:

- Docker memory 32768, step size `8x`, `max-retries: 2`.
- Pipe `sonarsource/sonarqube-scan:3.1.0`.
- Branches `master` and `dev_2020`: PHP scan then “other” scan.
- All pull requests: the same two steps.
- PHP scan inclusions `**/*.php`, Java heap requested at 8192m, `sonar.ws.timeout=1200`.
- Variables `SONAR_HOST_URL` and `SONAR_TOKEN`.

No deploy step in the pipeline. QA deploy is the manual `deploy.php` hard reset. Production deploy is **Unknown**.

## Branch map (remote heads, not all fetched)

| Branch | Role established from evidence |
|---|---|
| `dev_2020` | Inspected tip. Pipeline scans it. Latest merge is the PHP 8.2 upgrade PR #1747 |
| `master` | Default `HEAD`. Tip `da74b067` (2025-10-08) merges `prod_2020`. Pipeline scans it. Contents not diffed against `dev_2020` |
| `QA_2020` | Remote branch. `deploy.php` asks for lowercase `qa_2020` |
| `prod_2020` | Named only by the `master` merge message |
| `PHP_UPGRADE_8.2_2025_NEW_FEB25` | Merged into `dev_2020` |
| Feature branches | Listed in `project-plan.md`. Not diffed |

## Development process visible in the repo

- Work lands through Bitbucket pull requests (the tip commit cites PR #1747).
- Developers leave dated comments in PHP (`//20170802 - … - maanie`) and, recently, bullets in `NewKnowledgeBase.md`.
- No README, no migration tool, no seed files, no docker-compose.
- Local development is “point Apache at the tree and set `SERVER_NAME` so `system.php` loads a settings file”.
- Shared helpers of record are `project.functions.inc.php` and `ajaxfunctions.php`. Both are very large. A second helper file `project.functions.inc.pj.php` still defines `checkMemberAccess`.

## Monitoring

No application metrics agent was found. Operational signals that do exist:

- `sysLogin` rows on success and failure.
- `Global_LogUserNavigation` and `user.navigation.analytics.report.php`.
- `usage.log.php`.
- `custom_error.txt` / `custom.script.error.log.php` for the traceability worker (`ini_set('error_log', 'custom_error.txt')`).
- SonarQube on the pipeline.
- `exec_output.log` and `exec_return.log` are tracked files at the root. Their role was not read. They may be leftover command output. **Requires Confirmation** before deleting them.

## Discrepancies to resolve

1. `dev_2020` (2026-09-14) versus `master` (2025-10-08). Both are scanned by CI. Only `dev_2020` was read.
2. Training database is `sawis_training`; training API URL is the production API host.
3. `system.server.DEV.php` overwrites `localhost` several times. The live block is the last one (`10.0.0.120` / database `sawis`).
4. `deploy.php` branch name case (`qa_2020` vs `QA_2020`).
5. `system.server.LOCAL.php` is unused by the boot switch.
6. Credentials in git.
