# Technical documentation — sabullion (SA Bullion)

Evidence date: 2026-09-28. Source: `master` `a5cd347` unless a branch is named. Credential values are omitted on purpose.

## Repository

| Item | Value |
|---|---|
| Remote | `git@bitbucket.org:overdrive/sabullion.git` |
| Default branch | `master` at `a5cd347` (2024-03-12, Rachel Mogano, "repo update") |
| Other branches | `php8.1` at `bbf8065` (contained in `master`); `development` at `fcabece` (2021-03-01) |
| Working tree size | About 20,965 paths on `master`. Dominated by PHP, JavaScript, images, and vendored libraries |
| Root README | None on `master`. `development` README is two lines: "New SA Bullion WordPress website" and a temp host-h URL |

`master` versus `development`: 29 commits only on `master`, 891 only on `development`.

## What kind of codebase this is

`master` is a custom PHP site from the mid-2010s, maintained into 2024, with a Nemo back office and a client portal. It is not a framework application (no Laravel, CodeIgniter, Symfony, or Composer project at the root). WordPress appears in four places:

1. A full tree committed under `magazine/`.
2. A root `.htaccess` written for WordPress under `/sabullion/`, with no root `index.php` left to receive it.
3. A root All-in-One WP Migration archive, `sabullion.co.za-20240215-172951-8h6xci.wpress`.
4. A separate WordPress application on branch `development` (2021), not merged.

`.gitignore` ignores `/wp-admin/*`, `/wp-content/*`, `/wp-includes/*`, `/webadmin/SESSION/*`, `/dashboard/SESSION/*`, `/dashboard/pdf/*`, and `/pdf/*`. The ignore does not cover `magazine/wp-admin` or `magazine/wp-content`, and those are committed.

## Languages and libraries

- PHP. October 2023 commits say "PHP 8.1 update". `webadmin/index.php` is commented "PHP 8". No `composer.json` at the root and no `.php-version` file.
- JavaScript and CSS shipped as vendor trees (`vendors/`, `assets/`, dashboard assets). Bootstrap, DataTables, Chart.js, Flot, jQuery, Dropzone, and others.
- SQL is inline in PHP. No migration directory.
- PDF: mPDF under `vendor/mpdf` and older copies under `webadmin/includes/mpdf` and dashboard plugins. dompdf is also under `dashboard/api/plugins/dompdf`.
- Spreadsheets: PHPExcel.
- Mail: PHPMailer 5.2.28 under each Nemo framework folder, and `vendor/phpmailer`.
- HTML parsing for the price scrape: `webadmin/simplehtmldom_1_9_1`. The gold cron also uses curl directly.

## Application structure

```text
config/                 host switch, two PDO connections, VAT load
content/                PHP files that only assign marketing strings
helpers/                public form uploaders and ajax
templates/              public HTML
investment/ savings/ krugerrand/ gold-pricing/ partner/
mandate/ terms/ POPINotice/ scheduler/
                        one index.php per page, all the same shape
dashboard/              portal, api/, templates/, helpers/
webadmin/               Nemo screens, includes/*.cls.php, cron scripts
webadmin/_framework/    Nemo classes actually included by the screens
webadmin/_framework8.0/ second copy
webadmin/_frameworkX/   third copy
fim_2.0/                file integrity monitor
magazine/               WordPress
vendor/ vendors/        third-party libraries
```

A public page controller sets `$PageTemplate`, `$PageContent`, and `$PageTitle`, then requires `config/site.php`, `helpers/requests.php`, `helpers/functions.php`, and `templates/includes/begin.php` and `header.php`. Example: `investment/bullion-gold/index.php`.

`config/site.php` defines `ROOT`, `CONFIG`, `HELPERS`, `TEMPLATES`, `PAGE_CONTENT`, and asset URLs from `$SiteUrl`. It then requires `dbconnect.php` and queries `sysVat`.

## Databases

Two PDO handles, both created in `config/dbconnect.php` from `$_SERVER["SERVER_NAME"]`.

| Handle | Comment in code | Live database name (`sabullion.co.za`) | Live host |
|---|---|---|---|
| `$SA` | access database | `sabullio_accessDB` | `dedi773.jnb3.host-h.net` |
| `$DBH` | web database | `sabullio_web_db` | same host, different user |

Other hosts in the same switch: `localhost` (`sabullio_accessdb`, `sabulli1_web_db`), `lamp`, `dev.overdrive.co.za` (`sa_bullion` on `sql8.jnb1.host-h.net`, web DB on `sql38`), `sabullion.overdriveqa.co.za` (`sab_accessDB_2018` and `sabulli1_web_db` on `sql26.cpt3.host-h.net`), `sabullion-staging.overdriveqa.co.za` (access DB on `sql29.cpt3.host-h.net`, web DB on `sql26`).

Usernames and passwords are hardcoded in that file, including commented-out older passwords. They are not repeated here.

Tables named from queries read during this pass:

| Table | Used for |
|---|---|
| `sysVat` | VAT rate `dblVat` |
| `sysUser`, `sysSecurityGroup` | Staff login and groups |
| `sysMenuLevel2` | Admin menu labels |
| `sysAuditLog` | Audit list |
| `web_client_registration` | Applications, `strRegistrationStatus`, `strEntityType` |
| `tblDocuments` | `DocumentBank`, `DocumentPassport`, `DocumentResidentialAddress`, approve/reject flags |
| `web_client_detail` | Portal clients, `AurumNumber` |
| `web_client_debit_order` | Debit orders on statements |
| `tblProductPricing` | Premium (`dblPremium`) and silver product `1846` |
| `gold_price` | Historical prices for the calculator (`Date`, currency column) |
| `tblDate`, `web_gold_pricing` | Mismatch cron |
| `wp_postmeta` | Shop price write from the gold cron |

WordPress in `magazine/` has its own `wp-config.php`. That file was not opened for this note, so the magazine database name is **Unknown** from this pass. The gold cron's `switchBD($DATABASE_SETTINGS_WORDPRESS)` shows the admin app also has a WordPress connection profile inside Nemo settings.

There is no schema dump in git. Column lists above are only the columns the read code touched.

## Auth

**Public site.** No login.

**Dashboard.** `dashboard/index.php` redirects to `login.php` when `$_SESSION["USER"]->SYSTEM` is empty. Login posts through `dashboard/helpers/login_requests.php` and `dashboard/api/login/`. Session keys observed: `USER`, `GLOBAL_ADMIN`, `HOUSE`, `IFA`, `CLIENT`, `UNAPPROVED-CLIENT`.

**Web admin.** `webadmin/login.php` selects `sysUser` joined to an active `sysSecurityGroup` by email. On success it stores `SECURITYGROUPID` and sets `$_SESSION["GLOBAL_ADMIN"]->USER_TYPE = "GLOBAL_ADMIN"`. Password comparison itself was not line-read in this pass. **Unknown** whether passwords are hashed.

**Handoff.** `dashboard/login.php` keeps the session when the HTTP referrer contains `{SERVER_NAME}/webadmin` and global-admin login status is `Success`.

**Sessions on disk.** `.gitignore` ignores `webadmin/SESSION` and `dashboard/SESSION`.

No captcha, rate limit, or CSRF token was identified on the login paths during this pass. Public controllers call `ini_set('display_errors', 1)`.

## Background jobs

PHP scripts intended to be invoked on a schedule. Nothing in git says how (cron, panel, or manual).

| Script | What it does |
|---|---|
| `webadmin/cron.gold.price.php` | Scrape Sharenet, write WordPress `_price` / `_regular_price` / `vat_value` |
| `webadmin/after_hours.cron.gold.price.php` | Second gold run. Body not fully read |
| `webadmin/cron.silver.price.update.php` | Silver USD and USD/ZAR to product `1846` |
| `webadmin/cron.documents.reminder.php` | Email clients missing registration documents. Comment: every 48 hours |
| `webadmin/cron.outstanding.payments.php` | Email `Active Client - EFT outstanding` where once-off amount is greater than 0 |
| `webadmin/cron.client.balance.php` | Intended to fill a balances table. Query is limited to client id `1436` |
| `webadmin/cron.data.mismatches.php` | Dates in `tblDate` missing from `web_gold_pricing` |
| `fim_2.0/fim.cron.php` | FIM run. Includes `fim.cls.php`. Pauses via `sesFile.txt` |
| `magazine/wp-cron.php` | WordPress cron, if that site is hit |

`dashboard/helpers/cron.daily.php` and `dashboard/helpers/cron.documents.reminder.php` are further copies on the portal side.

## Integrations

**Sharenet.** `https://www.sharenet.co.za/v3/spot.php`. Curl with `CURLOPT_SSL_VERIFYPEER` false. **Implemented** in `cron.gold.price.php`.

**FNB.** `https://pay.ms.fnb.co.za/eCommerce/v2/validateTransaction?txnToken=`. API key is a hardcoded header in `webadmin/validateFNBPayment.php`. Success `responseCode` `000` redirects to `https://sabullion.co.za/checkout`; otherwise `https://sabullion.co.za/cart`. A sandbox `prepareTransaction` call is commented out in the gold cron and includes another key. Neither key is copied here.

**Mail.** PHPMailer. SMTP host is not centralised in `config/site.php` (those lines are commented). Where live mail is sent from is **Unknown** beyond the library being present.

**Rand Refinery, Nedbank.** Named in customer-facing copy only.

## FIM

`fim_2.0/fim.config.php` sets project name "SA Bullion", environment string `qa`, base URL `https://sabullion.co.za/fim_2.0/`, max runtime 60 seconds, and the live web database host. Sibling files: `fim.config_dev.php`, `fim.config_qa.php`, `fim.config_live.php`. `fim.cron.php` does not include a config file in its first lines. The class builds links to `fim.config_{environment}.php`. Which file production runs is **Unknown**.

The class walks directories, skips wildcard exclusions, compares to stored master rows, and can chmod as it records (directory `0755`, file `0644`).

## WordPress magazine

`magazine/` contains standard WordPress entry points (`wp-load.php`, `wp-login.php`, `xmlrpc.php`, `wp-cron.php`) and:

- Theme `sa_bullion_magazine` plus twentyfifteen, twentyfourteen, twentysixteen, twentyseventeen.
- Plugins: `advanced-custom-fields`, `acf-repeater`, `akismet`, `ricg-responsive-images`.

`wp-config.php` is committed. It was not quoted here.

Root `.htaccess` rewrite base is `/sabullion/` and the front controller is `/sabullion/index.php`. That does not match a vhost whose document root is this repository, and the target `index.php` is not in `master`.

## eCommerce folder

`eCommerce/` contains two logo PNGs and a `resources/images` copy of them. Shop behaviour that does exist is the FNB validator and the WordPress price meta writer, not this folder.

## Server checks

`server-checks/phpinfo.php` and `server-checks/fopen.php` are present. `phpinfo.php` on a public host would expose configuration. Whether the directory is web-reachable is **Unknown**.

## Build, CI, tests

- No root `composer.json`, `package.json`, Dockerfile, or PHPUnit config.
- No test directory was identified.
- `bitbucket-pipelines.yml` is not on `master`. It was edited many times in 2019–2020 (authors include Jaco Brits and Gael Musikingala) and still exists on `development`.
- `sonar-project.properties` and `sonarqube_scan.bat` were on the scan host's working tree and are not in git.

## Deployment

Inferred from hostnames only. The live `SERVER_NAME` is `sabullion.co.za` on host-h (`dedi773.jnb3.host-h.net`). QA is `sabullion.overdriveqa.co.za`. Staging is `sabullion-staging.overdriveqa.co.za`. An older dev path is `dev.overdrive.co.za/sabullion`. Local names used by the team were `localhost/sabullion_current_live` and `lamp/sabullion`.

No deploy script is on `master`. The 2019 pipeline history suggests Bitbucket Pipelines once uploaded the tree. That file is gone from `master`, so the current deploy path is **Unknown**.

## Security notes for reviewers

These are observations, not a pentest.

- Database passwords, old passwords in comments, and an FNB API key are in git.
- The price scraper disables TLS peer verification.
- Public pages enable display_errors.
- `server-checks/phpinfo.php` is in the tree.
- FIM can chmod the document root if it is run against it.
- Legal content files contain another company's terms.

## Development process visible in git

Work is direct commits to `master` and occasional branches. Messages are short ("repo update", "fix filters and last user/edit"). Authors on the recent `master` line include Rachel Mogano and Uriel Balela. Earlier pipeline and FIM commits include Jaco Brits, Gael Musikingala, JacquesOverdrive, and rachelmogano.

Dated copies of PHP files (`file.php20231124`) are the backup method visible in the tree, next to the Nemo copies `_framework`, `_framework8.0`, and `_frameworkX`.

## Local inspection note

The scan path `~/projects/sonar_scan/repos/sabullion` was not on the machine that wrote this document. Evidence is from a blobless clone of `a5cd347`, with a sparse checkout of PHP and content. Image, font, and the `.wpress` blobs were not downloaded. The `.wpress` archive was not opened, so its page list is **Unknown**.
