# Technical documentation — sarugby.online

Evidence date: 2026-09-28. Source: `master` at `8a0437d` (2020-01-07).

Secrets (database password, SMTP password, hard-coded login passwords) are present in the source. This document names the files and the hosts. It does not repeat the secrets.

## Repository

| Item | Value |
|---|---|
| Remote | `git@bitbucket.org:overdrive/sarugby.online.git` (cloned over HTTPS for this review) |
| Default branch | `master` |
| Other branches | None |
| Commits | `9815ef5` 2019-12-18 "initial commit of full repository"; `8a0437d` 2020-01-07 ".gitignore edited online with Bitbucket" (Heinrich du Plessis) |
| Tracked ini | `application/configs/application.ini` is listed in `.gitignore` and is still in the tree |

Root `.gitignore` also ignores `public/archives/` and `website/public/archives/`.

The task-creation listing included `sonar-project.properties` and `sonarqube_scan.bat`. They are not in `master`.

## Stack

| Layer | What the tree shows |
|---|---|
| Language | PHP. `mcrypt_create_iv` is called. That extension is not part of PHP 7.2 or later. The version deployed is **Unknown / Requires Confirmation**. |
| Framework | Zend Framework 1.11.10, vendored at `library/Zend`. `Zend_Version::VERSION`. |
| Front controller | `public/index.php` builds `Zend_Application` with environment `APPLICATION_ENV` or `production`. |
| Database | `pdo_mysql`. Ini: host `qadb.footprintapp.net`, database `teamwizz_SARU`, user `teamwizz_ADMIN`. The `qa` host name may mean a non-production server. **Unknown / Requires Confirmation**. |
| PDF | dompdf loaded from `dompdf/dompdf_config.inc.php`. A `public/dompdf` directory exists. |
| Mail | `Zend_Mail`. Accreditation uses SMTP `mail.sarugby.co.za` port 25, auth login, from `accreditation@sarugby.co.za`. Footprint reset uses `Zend_Mail` with no SMTP transport in the method read, from `support@footprint.net`. |
| UI | Server-rendered `.phtml`. Default layout pulls Sportsline theme CSS/JS from `/wp-content` and `/wp-includes`, plus Font Awesome from a CDN. jQuery 2.1.1 is under `public/wp-includes`. |
| Sessions | `resources.session.cookie_lifetime = 864000` (10 days) in `application.ini`. |
| Locale ini | `resources.locale.default = af_ZA`. Layout `lang` is `en-US`. |

`application.ini` sections: `production`, `staging` inherits production with no overrides, `testing` and `development` inherit production and set `display_errors` and `display_startup_errors` to 0. `public/index.php` then sets `display_errors` to 1 and `error_reporting` to `E_ALL|E_STRICT`, which overrides the ini for the request.

Timezone: `Africa/Johannesburg` in `public/index.php`, `NewsController`, and `TournamentsController`.

## Request path

Root `.htaccess`:

1. If HTTPS is off and the host does not start with `www.`, redirect to `https://www.{host}{uri}` (301). The condition is `RewriteCond !{HTTPS} off`, which is not the usual `%{HTTPS}` test. How Apache evaluates it on the real host was not executed. **Unknown / Requires Confirmation** that the redirect fires as intended.
2. URIs that do not start with `/public/` are rewritten to `public/{uri}`.

`public/.htaccess` is the standard Zend rewrite: existing files and directories are served, everything else goes to `index.php`.

`public/index.php` defines:

- `APPLICATION_PATH` → `../application`
- `APPLICATION_ENV` → environment variable or `production`
- `UPDATE_PATH` → `../public/archives`

Constants used in controllers but not defined in `index.php`: `UPDATES_PATH`, `IMG_PATH`, `AC_PATH`. Callers will warn or fail when those paths run.

Root `index.php` is an empty file. The application does not boot from it.

## Modules and routing

`resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"`.

Zend builds the controller class from the directory name. The classes on disk often use a different prefix, so those URLs will not dispatch unless something outside this repository renames the folders. That external rename was not found.

| Directory | Class prefix Zend expects | Class prefix on disk | URLs the code itself links to |
|---|---|---|---|
| `default` | `Default_` is not used; default module controllers are unprefixed (`IndexController`) | Unprefixed, except error controllers copied into other modules | `/`, `/login`, `/tournaments/...`, `/news`, `/cron/...`, `/ajax/...`, `/company/registration` |
| `bluecard` | `Bluecard_` | `Bluecard_` on the index and error controllers | `/bluecard` (module default index) |
| `menu` | `Menu_` | `Menu_` on index and session. Error controller is unprefixed `ErrorController` | `/menu/session/...` |
| `companiesold` | `Companiesold_` | `Companies_` on most controllers. `OrgController` and `AuthonController` are `Company_` | `/companies/login`, `/companies/register/...`, `/companies/` |
| `companyonl` | `Companyonl_` | `Company_` on index, org, and authon. Error controller is `Bluecard_ErrorController` | `http://www.sarugby.online/company/index/validateemail/id/{Member_id}` |

`companiesold/Bootstrap.php` declares `class Default_Bootstrap` and tries to force SSL when `SERVER_PORT` is not 443. Zend module bootstrap class names are `{Module}_Bootstrap`, so this class is not loaded as the module bootstrap. Same class name in `companyonl/Bootstrap.php` with an empty body. The application bootstrap `application/Bootstrap.php` declares `Bootstrap` and an empty `_initDbAdapter`.

`PG_Controller_Plugin_LayoutPicker` (`library/PG/Controller/Plugin/LayoutPicker.php`) sets the layout to the module name on every request. It is registered in `application.ini` as `resources.frontController.Plugins.LayoutPicker`.

Layout files in `application/layouts`: `admin.phtml`, `bluecard.phtml`, `default.phtml`, `food.phtml`, `menu.phtml`, `repo.phtml`, `Copy of companies.phtml`. There is no `companiesold.phtml` or `companyonl.phtml`. Several company actions call `disableLayout()`, so they avoid that missing file. Actions that do not disable the layout will look for a layout that is not there.

Ini keys `admin.resources.layout.layout`, `union.resources.layout.layout`, `referee.resources.layout.layout`, `gir.resources.layout.layout`, and `accreditations.resources.layout.layout` are not the syntax Zend uses for per-module layouts, and `LayoutPicker` does not read them.

## Application structure

```text
index.php                 empty
.htaccess                 HTTPS/www and rewrite into public/
application/
  Bootstrap.php
  configs/application.ini
  configs/models/         duplicate of application/models
  layouts/
  models/                 Zend_Db_Table classes
  modules/{bluecard,companiesold,companyonl,default,menu}/
library/Zend/             framework
library/PG/               project plugins
library/Other/            empty of code files in the listing
public/index.php          front controller
public/.htaccess
public/{assets,bootstrap2,dist,dompdf,fonts,global_assets,html,images,limitless,starters,wp-content,wp-includes}
website/                  second application, library, and public tree
test/                     Plesk placeholder page
```

Models are loaded as `Model_{Name}` from `application/models` by Zend's default model autoloader. The copies under `application/configs/models` are the same class names (`Model_Fixture` and others). They are not on the include path as a second autoload namespace in `application.ini`. `Copy of Fixture.php` also declares `class Model_Fixture`. If that file is autoloaded, PHP will fatal on a duplicate class. Default Zend model autoloading uses the class-to-file map `Model/Fixture.php` style under `models/` only when the resource autoloader is configured; this app instead appears to rely on files being included because the class files sit directly in `models/` as `Fixture.php`. How `new Model_Tournament` resolves was not executed. **Unknown / Requires Confirmation** whether an include or a custom autoloader outside the files read makes `models/*.php` load. The classes exist and are instantiated throughout the controllers, so some load path works in the environment this code was written for. A stock ZF1 module autoloader maps `Model_Tournament` to `models/Tournament.php`. That matches these filenames.

`Model_Credentials` and `Model_DbTable_Countries` are instantiated and are not in the model file list. `Model_AccreditationsApplications`, `Model_AccreditationZone`, `Model_AccreditationQueing`, and `Model_Assessments` are instantiated from `CronController` and are not files under `application/models`. Those classes are missing from this tree.

## Data

No `CREATE TABLE` scripts are in the repository. Table names come from `protected $_name` on the models that exist.

| Model | Table | Used for |
|---|---|---|
| `Model_Tournament` | `tournament` | Competitions |
| `Model_Fixture` | `Fixture` | Matches |
| `Model_FixtureScores` | `fixture_score` | Scores |
| `Model_Squad` | `squad` | Teams in a tournament |
| `Model_Profile` | `profile` | People, including Footprint login profile |
| `Model_GirProfile` | `gir_profile` | Alternate person store; joins `co_credetials` and `ef_unions` |
| `Model_Logs` | `TournamentLogs` | Logs / pools |
| `Model_MatchReport` | `Match_Report` | Match report rows and player time |
| `Model_ComentryAndCiting` | `ComentryAndCiting` | Commentary and citing |
| `Model_NewsArticles` | `News_Articles_SA_Rugby` | News |
| `Model_Positions` | `positions` | Positions (the Blue Card form does not query this model; the call is commented out) |
| `Model_Unions` | `ef_unions` | Unions |
| `Model_Country` | `countries` | Countries |
| `Model_Ethnicity` | `Ethnicity` | Ethnicity |
| `Model_CompanyProfile` | `Companies_Profile` | Companies |
| `Model_UserProfile` | `Company_Members` | Company people |
| `Model_CompanyEvents` | `Company_Events` | Company-event links |
| `Model_AccreditationList` | `Accreditation_List` | Events / accreditation definitions |
| `Model_AccreditationPermits` | `Accreditation_Permits` | Vehicle permits |
| `Model_AccreditationGates` | `EventGates` | Gates |
| `Model_AccreditationLoadingZones` | `Accreditation_Loading_Zones` | Zones |
| `Model_AccreditationLoadingServices` | `Accreditation_Load_Services_Types` | Load service types |
| `Model_AccreVehicleTypes` | `vehicletypes` | Vehicles |
| `Model_AccreCompanyTypes` | `Accre_Company_Types` | Company types |
| `Model_BBEELevel` | `BBEE_Level` | B-BBEE level |
| `Model_BBEETypes` | `B_BBEE_Types` | B-BBEE type |
| `Model_Bluecard` | `BlueCard_Consents` | Blue Card submissions |
| `Model_TournamentFoodSession` | `Tournament_Foodsessions` | Catering sessions |
| `Model_FoodSessionItems` | `SessionFood_Items` | Menu items |
| `Model_MenuVenues` | `Menu_Venues` | Venues |
| `Model_Nutritions` | `Nutritions` | Nutrition rows |
| `Model_Allergens` | `Allergens` | Allergens |

Tables named in SQL or adapters but without a model file in this tree:

| Table | Where |
|---|---|
| `co_credetials` | Footprint auth adapter (spelling as in the code) and joins from `GirProfile` |
| `CompaniesLogin` | Company auth adapter. `Companies_LoginController` also updates through `Model_CompanyProfile` on `MID`. Whether `CompaniesLogin` is a view over `Company_Members` is **Unknown / Requires Confirmation**. |

JSON documents under `public/archives` (gitignored, so not in a fresh clone) are part of the read path: `lastupdate.txt`, `tournaments/{TID}.json`, `tournaments/years/{year}.json`, `tournaments/list.json`, `tournaments/squads.json`, `Logs/{TID}.json`, `squads/{SID}.json`, tournament and squad JPEGs, Blue Card PDFs.

`public/178.json` and `public/209.json` exist and begin with the text `Array` plus a timestamp, not with JSON. They look like accidental dumps.

## File cache and images

`IndexController` and `AjaxController` decode base64 image columns (`SmallImage`, `TournImage`, `SquadImage`, `SmallerImg`, `Icon`) and write JPEGs under `UPDATE_PATH`. The base64 helper splits on a comma and decodes the second part, which matches a `data:*;base64,` prefix. Values without a comma decode an empty segment.

## Integrations

### AppWizard

`CronController::senddata_to_Api` POSTs multipart form data to `https://fsrugby.appwizard.co.za/scripts/AppWiz.dll`.

Fields: `G` = `Guest`, `A` = `acrSARugby`, `Barcode`, `Name` (first name), `Surname`, `IDNo`, `Category`, `Zone` (zone numbers separated by spaces), `TheFile` (picture field from the application row).

Called only when the queue row's `AC` equals 14. Timeout 86400 seconds. The HTTP status is not branched on. A commented URL `https://footprintapp.net/test/recievedata` is inactive.

Authentication toward AppWizard: none in the curl options beyond the `A` field. **Unknown** whether that field is a credential.

### Footprint PDF hosts

| Caller | URL |
|---|---|
| `TournamentsController::getmatchreportAction` | `https://footprintapp.net/pdf/matchreport/id/{id}` |
| `Companies_IndexController::getpermitpdfAction` | `https://www.footprintapp.net/pdf/getpermit/id/{id}` |
| `CronController::addtozip` | `{http or https}://{current host}/pdf/a6/id/{AAID}` |

This repository has no `pdf` controller. Those routes are external or missing.

### Mail

Accreditation messages: SMTP `mail.sarugby.co.za`, from `accreditation@sarugby.co.za`, display name "Registration at sarugby.co.za", subject "SARugby Notification". The password sits in `Companies_LoginController::sendMail`, `RegisterController::sendMail`, and `PermitsController::sendMail`.

Footprint messages: `Zend_Mail` default, from `support@footprint.net`, display name "SARU E-Filing".

Blue Card `sendMail` is further down `bluecard` `IndexController` (from line 222). It was not copied here. It attaches the concussion files.

## Auth

Two Zend_Auth adapters in one PHP session:

1. Footprint: table `co_credetials`, identity `Username`, credential `Password`, plus the `FPLogin` pre-check.
2. Company: table `CompaniesLogin`, identity `Email`, credential `password`.

Neither adapter sets a credential treatment of MD5 inside Zend. The controllers hash before `setCredential`, so the adapter does a plain compare against the already-hashed value.

Both logins contain a backdoor string that substitutes the stored password. See business rules BR-015 and BR-028. Rotate those secrets if this tree was ever deployed. Do not treat the backdoor as an approved rule.

Company area identity is whatever row the adapter returned. Later code reads `->CID` and `->MID` as object properties.

`resources.frontController.params.displayExceptions = 0` in production. `public/index.php` still displays PHP errors.

## Background work

No queue worker, no `crontab`, no supervisor config, no Windows service file. The jobs are ordinary HTTP actions:

| Action | Effect |
|---|---|
| `/ajax/getdata` | Export tournaments, logs, squads changed since `lastupdate.txt` |
| `/cron/queing` | PDF + optional AppWizard POST for the accreditation cue |
| `/cron/matches` | Advance fixtures for open tournaments |
| `/cron/getaccre` | Call `ApplyAccreditationNow` (method not in the file) and delete `Model_AccreditationQueing` rows |
| `/cron/autorun` | Empty |
| `/cron/logs`, `/cron/view`, `/cron/getfixtues` | Present on `CronController`. Not fully read. |

`queingAction` raises memory to the controller default and `init` of `CronController` sets `memory_limit` 800M and `max_execution_time` 3000 on the class file (via `ini_set` / `set_time_limit` at the top).

## Storage

- MySQL for records.
- `public/archives` for generated JSON, images, accreditation photos, Blue Card source PDFs.
- Permit PDFs written under `AC_PATH/loadin/pdf/` when that constant exists.
- Accreditation zip/PDF output under `IMG_PATH/accre/pdfs/` inside `addtozip`.

No S3, Redis, or queue broker appears in the configuration that was read.

## Deployment and environments

No Dockerfile, no CI config, no deploy script. `staging`, `testing`, and `development` ini sections do not change the database. `test/` is a Plesk default page ("If you are seeing this message, the website … is not available"), which suggests the host was a Plesk server at some point. Provenance: **Inferred** from `test/index.html` copyright line "Plesk International GmbH" 1999–2018.

`website/` repeats `application`, `library`, and `public`. It was not deployed by any script in the repo. Purpose **Unknown / Requires Confirmation**.

The root rewrite assumes the vhost document root is the repository root, not `public/`. If the vhost already points at `public/`, the root `.htaccess` never runs.

## Security notes for reviewers

Observed in code, not a penetration test:

- Database and SMTP passwords are committed in `application/configs/application.ini` and in company mail methods.
- MD5 passwords; reset email contains the new company password.
- String-concatenated SQL predicates.
- Unauthenticated export and cron actions.
- `display_errors` enabled in the front controller.
- Hard-coded login bypass on both auth controllers.
- `companyonl` `testAction` and `companiesold` `IndexController::testAction` query a hard-coded South African ID number and print the row.

## Development and testing

No PHPUnit, no composer.json, no npm manifest, no README, no contribution guide. `library/Zend` is committed rather than installed by Composer.

The `test/` directory is not a test suite. Controller copies named "Copy of" and "test" are manual leftovers.

`public/error_log` is tracked. It was not used as a requirements source.

## Monitoring

No application log configuration beyond Zend's error controller pattern (`ErrorController` in each module, with `getLog`). Whether a `Zend_Log` writer is configured was not seen in `application.ini`. The ini does not register a log resource. Sonar files from the scan host are not in Git.
