# Technical documentation — knowmorescan (Knowmore)

Evidence date: 2026-10-10. Source: `git@bitbucket.org:overdrive/knowmorescan.git`, `master` at `71190cf` (2022-06-02).

This document describes the tree that was inspected. It does not restate product policy; that is in `prd.md` and `business-rules.md`.

## Repository

- Remote: `git@bitbucket.org:overdrive/knowmorescan.git`
- Default branch: `master` (`origin/HEAD`)
- Other remote branch: `origin/backend-tweaks` (merged by `71190cf`)
- Sonar project key file: `sonar-scanner.properties` sets `sonar.projectKey= knowmore` including a leading space after `=`
- No root README. `cms/README.md` is the upstream CodeIgniter 4 text
- `.gitignore` ignores session, PDF, import, and `training/dcontent/` paths. It does not ignore `.env`

## Application structure

The repository is one document root with several applications side by side.

| Path | What it is |
|---|---|
| `*.html` | Static export of the WordPress / Elementor / GeneratePress site. Export plugin comment at the end of the files |
| `*.php` at the root | Front controllers: login, registration, content iframe, email tracking, forgot password |
| `training/` | Learner portal |
| `tools/` | Business-tool engines and a nested `tools/webadmin/` |
| `webadmin/` | Nemo staff admin and cron |
| `cms/` | CodeIgniter 4. Web root is intended to be `cms/public` (stock README). `spark` is present |
| `layouts/` | Shared learner and admin layout includes |
| `modules/` | Side modules (login, learner ads, reCAPTCHA library, user-type check) |
| `db_updates/` | Five hand-run SQL scripts for CMS tables |
| `fim_2.0/` | File-permission walker and `fim.config.php`, `fim.config_dev.php`, `fim.config_qa.php`, `fim.config_live.php` |
| `vendor/` | Root Composer install (dotenv, AWS SDK) |
| `css/`, `js/`, `images/`, `fonts/`, `flowplayer/`, `htmltopdf/` | Front-end assets and PDF/video helpers |

Nemo classes are the `_nemo.*.cls.php` files copied under the root, `webadmin/`, `training/`, and `tools/`. They are not a single shared package. Comments show the same class being edited in more than one copy.

## Frameworks and languages

- PHP, mysqli, session files on disk.
- Nemo is an in-house page framework (`NemoBasic`, `NemoDatabase`, `NemoPassword`, `NemoEmail`, list/detail/menu classes). It is not Composer-packaged.
- CodeIgniter 4 under `cms/` (`codeigniter4/framework` in `cms/composer.json`). PHP constraint `^7.3 || ^8.0`. Auto-routing is off.
- Front-end of the marketing site is exported HTML/CSS/JS (jQuery, Elementor, GeneratePress, SmartMenus). It is not a build pipeline in this repo.
- CMS views are PHP templates (Bootstrap-style admin chrome).
- JavaScript password-strength check is emitted by `NemoPassword::validatePasswordJS`.

PHP version comments: 5.6 compliance (2015), PHP 7 (2018, `cat.class.php`). Production version is **Unknown / Requires Confirmation**.

## Runtime configuration

`_system.php`:

- Loads `vlucas/phpdotenv` from `DOCUMENT_ROOT/Knowmore` (not from the repository root path, and not from `.env.default`'s keys).
- Reads `PROJ_ENV`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`, `PROJ_SESSION_PATH`, `PROJ_ENV_ROOT`.
- Builds `$DATABASE_SETTINGS` and connects with `mysqli_connect`. On failure it prints a critical message that includes `mysqli_connect_error()`.
- Loads every `sysSettings` row into `$SystemSettings`.
- Sets `session.gc_maxlifetime` to 7200 and `session.save_path` from config, then `session_start()`.
- Rewrites host names as described in the business rules.
- Hardcodes SagePay Pay Now service keys for two hosts. Those values are secrets. They should not be copied into docs or tickets. **Requires Confirmation** that they are still valid and that they belong in source control.
- Runs `find`/`chmod` against `training/dcontent/` on each include of `_system.php`.

Checked-in env examples do not match that code:

| File | Names present |
|---|---|
| `.env.default` | `APP_ENV`, `DATABASE_DNS`, `DATABASE_USER`, `DATABASE_PASSWORD`. Commented Google keys. A space-padded `DATABASE_PASSWORD` line is also in the file |
| `.env.txt` | Same three database names as `.env.default`, with local example values |
| `.env` | Present in the clone. Not quoted here. `.gitignore` does not exclude it |

`cms/app/Config/Database.php` is the stock CI4 file: MySQLi, host `localhost`, empty user, empty password, empty database, port 3306. The CMS does not call dotenv in that file.

`cms/env` exists as the CI4 env template. It was not fully reviewed.

`fim_2.0/fim.config_live.php`, `fim.config_qa.php`, and `fim.config_dev.php` are environment-specific. Their secrets, if any, were not copied here.

## Database

Engine: MySQL-compatible, InnoDB, in the CMS scripts (`utf8mb4` tables). Nemo uses `mysqli`.

CMS tables created by `db_updates/`:

| Script | Table | Purpose |
|---|---|---|
| `update.1.sql` | `cmsarticlecategory` | Category names and seed rows |
| `update.2.sql` | `cmsarticles` | Articles plus three seed articles. Status enum `Draft`, `Pending`, `Published`, `Archived` |
| `update.3.sql` | `cmsarticletags` | Article-to-category link, unique on the pair |
| `update.4.sql` | `cmssubcategory` | Subcategories with seed rows |
| `update.5.sql` | `cmssysuser` | CMS users. `role` default `cmsadmin`. Seed rows include password hashes |

`update.5.sql` contains real-looking email addresses and password hashes. They are historical seed data. They are intentionally not repeated in Silicon Ark. **Requires Confirmation** whether that script was ever applied to production and whether those accounts should be removed.

Model table names do not match the SQL file names in letter case:

| Model | `$table` | SQL script |
|---|---|---|
| `ArticleModel` | `cmsArticles` | `cmsarticles` in `update.2.sql` |
| `UserModel`, `LoginModel` | `cmsSysUser` | `cmssysuser` in `update.5.sql` |
| `SecurityGroupModel` | `sysSecurityGroup` | Not created in `db_updates/`. This is the Nemo admin table. Allowed fields are `strSecurityGroup`, `blnActive`, `strLastUser`, `dtLastEdit`. A `passwordHash` callback is copied onto the model and does nothing unless `strPassword` is present, which is not an allowed field |

On a Linux MySQL with case-sensitive table names, `cmsArticles` and `cmsarticles` are different tables. Whether production folds case is **Unknown / Requires Confirmation**.

Nemo tables used by the code and not created in this repo include, among others: `tblLearner`, `tblUserType`, `tblUserCategory`, `tblPractice`, `tblCourse`, `sysUser`, `sysSecurityGroup`, `sysSecurity`, `sysMenuLevel1`, `sysMenuLevel2`, `sysSettings`, `sysPassword`, `sysLogin`, `dwfBatResults_2020`, `tblQQ_2020`, `gapSCPQuestion`, `gapPoPIQuestion`, and the BAT tables named in comments inside `bat.class.php` (`tblQuestion`, `tblCluster`, `tblPillar`, `tblOption`). This is not a complete schema.

There is no migration runner for `db_updates/`. They are raw SQL files. CI4's `spark` migrations were not found as project migrations under `cms/app/Database` in this pass (the directory may contain only framework stubs).

## Authentication and sessions

- Learner: `$_SESSION[LEARNER]` set in `login.php` / `second.wp.php`. Password check `NemoPassword::checkPassword("Learner", ...)`.
- Webadmin: `$_SESSION[USER]` in `webadmin/login.php`, joined to `sysSecurityGroup`.
- CMS: CI4 session keys `isLoggedIn`, `role`, `UserID`. Filter `auth` on the `admin` group. Filter `noauth` on `login`. Hash verify in `cms/app/Validation/UserRules.php`.
- Password reset interval default for Nemo: `60 MINUTE` in the settings insert.
- Session files: path from env; gitignore covers `webadmin/SESSION/*`.

`login.php` calls `session_destroy()` before including the Nemo stack, then `session_start()` again on the Login action.

## Authorisation defects visible in code

- `Article` refuses anyone whose role is not `cmsadmin`.
- `User` and `SecurityGroup` use `role != "cmsadmin" && isLoggedIn != true`, so any logged-in session passes.
- `SecurityGroup::add` is routed as `admin/add`, not `admin/security-group/add`.
- Self-delete is skipped but the flash message still says the user was deleted.
- `Routes.php` calls `setDefaultController` twice (`Login`, then `UserList`). The second call wins. No `UserList` controller was found.

## HTTP surface (high level)

Public HTML is the file name without `.html` when the rewrite matches.

Important PHP entry points:

- `second.wp.php` — login post target from `login.html`
- `login.php` — learner login with message ids
- `register.ph3.php` — registration iframe
- `forgot.password.php`, `forgot-pw.php`, `forgot.password.wp.php`
- `training/index.php` and the other training scripts
- `tools/*.engine.php` and `tools/css.survey.php`
- `webadmin/login.php` and `webadmin/*.php`
- `webadmin/cron.daily.php`, `webadmin/cron.hourly.php`, `cron.hourly1.php` … `cron.hourly8.php`
- `cms/public/index.php` — CI4 front controller
- `email.read.receipt.php`, `email.read.online.php`
- `phpinfo.php` — present at the root

CMS routes are listed in `prd.md` workflow W9. `setAutoRoute(false)`.

## Background work

No queue library is referenced in application code. Work is cron PHP:

- Hourly script cases cover client import, invoice and order mail, CPD import, certificates, course start and completion, workshops, notices, CSS invite and reminder, CPD notification, IBC and AML registration emails.
- Daily script cases cover invites (PFA, IFA, IFA Private), course-start mail, AML learner processing, IBC learner processing, notices, apologies.
- FIM cron: `fim_2.0/fim.cron.php` with `fim.cls.php`.

How the host schedules these files is **Unknown / Requires Confirmation**. Comments mention Hetzner and cron `SERVER_NAME` bugs (2017).

## Payments

- SagePay Pay Now hosted POST from `training/purchase.courses.php` and `training/purchase.workshops.php`.
- Netcash hosted POST from `training/purchase.php` and `training/purchase.omw.php`.
- Return path: `training/transactions.php`.
- Service keys: hardcoded in `_system.php` (see Runtime configuration).

## Mail

`webadmin/_nemo.email.cls.php` and includes of it from cron and registration. Transport (SMTP host, from-address, provider) is **Unknown / Requires Confirmation**. It is likely a `sysSettings` value or hardcoded inside the email class; the class was not fully read.

## Files and storage

- Learner downloadable content: `training/dcontent/` (gitignored, chmod'd at bootstrap).
- SLA documents: `webadmin/documents/SLA/`.
- Generated PDFs: `PDF/` (gitignored), FPDI under `webadmin/fpdi` and `tools/fpdi`, `htmltopdf/`.
- CMS uploads: article seed HTML points at `http://localhost/cms/public/uploads/...`. The upload route is Dropzone (`dropzone/upload`).
- Profile pictures: `training/profilepictures/` and a session field populated from a column whose name is built as `Profile:PicturePath`.

## AWS

Root `composer.json` requires `aws/aws-sdk-php` ^3.204, and `vendor/` contains the SDK. Searches under `cms/app`, `webadmin`, `training`, and `tools` found no `S3Client` or `Aws\` import in application code. Usage is **Unknown / Requires Confirmation**. It may be dead weight from a scan remediation, or a call site outside those directories.

## Security notes (observations, not a pentest)

- `.htaccess` forces HTTPS, disables directory indexes, denies `wp-config.php` and `.user.ini`.
- `.user.ini` prepends a Wordfence WAF file on a host-specific path (`/usr/www/users/qacelmtbmy/wordfence-waf.php`). That path will not exist on another machine.
- Secrets in the tree: `.env` is not gitignored; SagePay keys are in `_system.php`; `db_updates/update.5.sql` contains password hashes; `.env.default` contains a commented Google API string.
- `phpinfo.php` and `test.php` are in the document root.
- `content.php` iframes `http://` (not https) `www.celestis.co.za`.
- SQL in the gap and BAT classes concatenates ids into query strings. `_system.php` comments say a 2021 SonarQube pass targeted SQL injection and log injection. Residual concatenation is still visible in `gap.SCP.class.php` and `training/index.php`.
- CMS article HTML is stored and edited as content (`txtContent`). Output encoding was not verified.
- `register.ph3.php` comments say reCAPTCHA was removed from the include list.
- `tools/Xadfs-idp-hosted.php` is SimpleSAMLphp example metadata, not a completed SSO integration.

## Hosting clues

- Public canonical host in the HTML: `https://www.knowmore.co.za/`.
- QA host strings: `www.qa-celestis.co.za` in `_system.php`; `qa-omw-newbuild.co.za.www8.jnb1.host-h.net` inside Yoast JSON-LD on the static pages (xneelo / host-h style).
- `.user.ini` path is the same host-h user-directory pattern.
- Comments in `_system.php` mention Hetzner (2017) and a password reset after a breach (2017-11-02). That comment is historical. It is not evidence of the current host.
- No Dockerfile, no Terraform, no Bitbucket pipeline file was found.

## Development and testing

- CMS: `spark`, `phpunit.xml.dist`, PHPUnit in `cms/composer.json` require-dev. No `cms/tests` directory was found. `cms/app/Config/Database.php` `$tests` group is SQLite `:memory:` with prefix `db_`, which is the framework default.
- Nemo: no PHPUnit suite was found.
- Sonar scanner properties exist. A pipeline that runs the scanner was not found.
- Local database credentials in `.env.txt` are `localhost` / database `test`.

## Dependencies (direct)

Root `composer.json`:

- `vlucas/phpdotenv` ^5.3
- `aws/aws-sdk-php` ^3.204

`composer.lock` is committed.

CMS `composer.json` is the CodeIgniter 4 framework distribution (kint, laminas-escaper, psr/log; dev: phpunit, faker, php_codesniffer, predis, vfsstream).

Third-party code also vendored as directories rather than Composer: `webadmin/fpdi`, `webadmin/Excel`, `flowplayer`, `htmltopdf`, `modules/recaptchalib.php`.

## Monitoring

No application log aggregator, health endpoint, or APM configuration was found. Login attempts are written through `tblLoginInsert` into what the learner home reads as `sysLogin`. `webadmin/usage.log.php` is an admin screen for a usage log. FIM writes a `sesFile.txt` while it walks the tree (`fim.cls.php`).
