# Technical documentation — permits.mypass.zone

Evidence date: 2026-10-07. Source: `master` at `16d1793` (2022-07-01).

Secrets in source (database password, SMTP password, shared PIN, a sample identity number, a personal test mailbox) are named by file and not copied.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Language | PHP | `public/index.php` |
| Framework | Zend Framework 1.11.10 | `library/Zend/Version.php` |
| Style | Zend_Application, one module, `Zend_Controller_Action`, `Zend_Db_Table` | `application/` |
| Database | PDO MySQL (`pdo_mysql`) | `application/configs/application.ini` |
| Document root | `public/` | `public/.htaccess`, `public/index.php` |
| Timezone | `Africa/Johannesburg` | `public/index.php` |
| Locale config | `af_ZA` | `application.ini` |
| UI theme | Limitless (local copies under `public/limitless` and `public/global_assets`) | Login and layout views |
| PDF | dompdf under `public/dompdf` | `IndexController::getpermitpdfAction` |
| Random bytes | `mcrypt_create_iv` | Registration, password reset, staff passwords |
| Tests | None found | No PHPUnit config, no test directory |
| Package manager | No `composer.json` at the root | Zend and dompdf are vendored |

PHP version on the server is **Unknown / Requires Confirmation**. `mcrypt` in the registration path means the code as written does not run on PHP 7.2 or later without a replacement.

`public/index.php` sets `display_errors` to on and `error_reporting` to `E_ALL|E_STRICT`. The ini file sets `display_errors` to `0` for production, staging, testing, and development. The front controller wins for the PHP settings it sets before bootstrap. Controllers also call `ini_set('display_errors', 1)`.

## Request path

1. Apache rewrite: existing files are served; everything else goes to `index.php` (`public/.htaccess`).
2. `index.php` defines `APPLICATION_PATH`, `APPLICATION_ENV` (default `production`), and `UPDATE_PATH` (`public/archives/`).
3. `Zend_Application` boots `application/configs/application.ini`.
4. `Bootstrap` has an empty `_initDbAdapter`. The DB adapter comes from `resources.db`.
5. Front controller module directory is `application/modules`. The only module is `default`.
6. Plugin `PG_Controller_Plugin_LayoutPicker` sets the layout to the module name (`default`), which selects `application/layouts/default.phtml`.
7. Default layout path is `application/layouts`, default layout name `default`.

A commented block at the top of `index.php` would have stopped the request and shown a maintenance image. It is not active.

Routes are the Zend default: `/{controller}/{action}/{param}/{value}`. There is no custom router file.

## Configuration

`application.ini` sections: `production`, and `staging`, `testing`, `development` inheriting production. Only the development section adds `displayExceptions = 0`, which production already sets.

Database: adapter `pdo_mysql`, host `localhost`, database name `teamwizz_SARU`. The username and password are hardcoded in this file. They are not repeated here.

Also set: library include path, bootstrap class `Bootstrap`, empty `appnamespace`, session cookie lifetime `864000`, autoload namespace `PG_`.

Layout names `admin`, `union`, `referee`, `gir`, and `accreditations` are configured and have no layout files. **Inferred** leftovers from the parent SARU application.

`APPLICATION_ENV` is not set in the deploy scripts. The app runs as `production` unless the server sets the variable.

## Controllers and routes

All under `application/modules/default/controllers/`.

| Controller | Session gate | Actions that do work |
|---|---|---|
| `LoginController` | None. Redirects to `/` if `EID` is already set | `index`, `forgot`, `reset`, `logout`, `external`, `companypic`, `uploadremotecompany`. `LoginNow` is not an action |
| `IndexController` | `EVENT.EID`, then redirect to `/walkins` | `index`, `getdata`, `getpermitpdf`, `newcompany`, `validateemail` (empty), `test` are all behind that redirect |
| `WalkinsController` | `EVENT.EID` | `index`, `search`, `new` |
| `PermitsController` | `EVENT.EID` | `questions`, `profile`, `profiletest`, `create`, `addpermit`, `savepermit`, `duplicatepermit`, `cancel`, `emailtest` |
| `RegisterController` | None | `index`, `newcompany`, `validateemail` |
| `UsersController` | `Zend_Auth` | `index`, `add`, `profile` |
| `CompanyController` | `Zend_Auth` | `profile`, `upload`. `getmembers` is empty |
| `ErrorController` | None | `error` |
| `AuthonController` | None | Empty actions |
| `OrganisationController` | None | Empty actions |
| `EventsController.php` | Class is `Companies_EventsController` | Will not match the default dispatcher class name `EventsController` |
| `OrgController.php` | Class is `Company_OrgController` | Will not match `OrgController` |

`PermitsController` declares `$identity` and never sets it. Actions that read `$this->identity->CID` or `MID` (`addpermit` email lookup, `profiletest`, `savepermit`, `duplicatepermit`, `cancel`) hit that gap.

## Data

There is no migration directory. Table and view names come from `Zend_Db_Table` classes and from SQL fragments.

Used by the walk-in and registration paths:

| Name | Kind | Used for |
|---|---|---|
| `Accreditation_Permits` | Table | Permit rows. Primary key used as `PID` |
| `Permits_view` | View | Dashboard, profile, and the email lookup |
| `Permit_Status` | Table | `getStatus` only. Not called by the walk-in actions |
| `Accreditation_List` | Table | Event row, key `AC`. Dates, load windows |
| `Accreditation_Loading_Zones` | Table | Zones for an event (`AID` / `ZID`) |
| `Accreditation_Load_Services_Types` | Table | Service type dropdown |
| `EventGates` | Table | Gates for an event (`AC`) |
| `vehicletypes` | Table | Vehicle type dropdown |
| `Companies_Profile` | Table | Company |
| `Company_Members` | Table | People. Key `MID` |
| `Company_Events` | Table | Company linked to an event. `CEID`, `passkey`, `EID` |
| `AccreditationMemberProfile` | Table | Joined for the admin's name and contact on the company list |
| `CompaniesLogin` | View or table | Email/password adapter only |
| `Accre_Company_Types` | Table | Registration company types |
| `BBEE_Level` | Table | Registration B-BBEE levels |
| `countries` | Table | Registration countries |
| `B_BBEE_Types` | Table | Model exists. Registration does not use it |

Permit columns written or read by name in the controllers: `PID`, `EID`, `CID`, `MID`, `Status`, `PermitNr`, `PermitRefSerial`, `DateCreated`, `RequestDate`, `TimeIn`, `TimeOut`, `LoadType`, `Accupants`, `LoadinActiveDate`, `LoadOutActiveDate`, `ApplicantModifiedDate`, `ApplicantModifiedBy`, `CancelledDate`, `CancelledBy`, `CancelMessage`, plus the approval and decline columns that duplicate clears (`Gate`, `Zones`, `ApprovedBy`, `ApprovedDate`, `DeclinedBy`, `DeclinedDate`, `DeclinedMessage`, `ConfirmmedBy`, `ConfirmedDate`, `DateModified`, `AdminModifiedBy`, `UserModified`).

`Accupants` is a JSON array of objects with `Type`, `Name`, `Surname`, `ID`, `Driver`, `MID`, and sometimes `Mobile` on the driver.

Event date lists are JSON arrays of `d/m/Y` strings.

Models that point at rugby, food, news, and GIR tables are in `application/models/` and are not called by the walk-in controllers. Sharing of `teamwizz_SARU` with other products is **Unknown / Requires Confirmation**.

`updateData` on permits echoes the payload (`echo "<pre>Update"` and `print_r`) before writing. That output is part of the HTTP response for save, cancel, and the permit-number update inside create.

## Files written at runtime

| Path | Writer |
|---|---|
| `public/archives/companies/{cid}_image.jpg` | `CompanyController::uploadAction`, `LoginController::uploadremotecompanyAction` |
| `public/archives/accreditation/{memberId}_image.jpg` | `UsersController` add and profile |
| `AC_PATH/loadin/pdf/permit_{id}.pdf` | `getpermitpdfAction`. `AC_PATH` is not defined in this repository |

`UPDATE_PATH` is `public/archives/`. The company and member image paths are under that directory.

## Mail

Four copies of `sendMail` (login, register, permits, index) use `Zend_Mail_Transport_Smtp`.

| Setting | Value |
|---|---|
| Host | `mail.sarugby.co.za` |
| Port | `25` |
| Auth | `login` |
| Username | `accreditation@sarugby.co.za` |
| Password | Hardcoded in each method. Not copied |
| From | `accreditation@sarugby.co.za`, "Registration at sarugby.co.za" |
| Subject | "SARugby Notification" |

`LoginController` and `RegisterController` and `IndexController` set `SMTPSecure = tls` on the mail object. `PermitsController::sendMail` leaves that line commented out. Whether port 25 and TLS are a valid combination for this host is **Unknown / Requires Confirmation**.

`PermitsController::postEmailToFp` would POST `Email`, `FullName`, and `Content` to `https://footprintapp.net/email/send` with a timeout of 86400 seconds. The call is commented out.

## Integrations

- **MySQL** on `localhost`, database `teamwizz_SARU`. Credentials in `application.ini`.
- **SMTP** as above.
- **supplier.sarugby.online** — registration email link only. This app does not HTTP-call it.
- **sarugby.online** — older validation link inside the unreachable `IndexController::newcompanyAction`.
- **footprintapp.net** — commented email POST, and a GET of permit HTML for the unreachable PDF action (`https://www.footprintapp.net/pdf/getpermit/id/{id}`).
- **Google Fonts and Font Awesome CDN** — login page stylesheets.
- **AWS CodeDeploy and S3** — pipeline deploy. See below.

No queue, cache, or background worker is configured. There is no cron entry in the repository.

## Auth model

Two mechanisms, only one of which the login form uses:

1. **Event session.** Namespace `EVENT`, property `EID`. Set by the PIN or by `/login/external`. Checked by walk-ins, permits, and index.
2. **Zend_Auth.** Identity row from `CompaniesLogin`. Checked by users and company. Written only by `LoginNow`, which the form does not call.

Password reset and registration write `Company_Members`, not the event session.

The company passkey is not an auth identity. It is a column compared in the browser and again in `getRow`.

`/login/uploadremotecompany` and `/login/companypic` do not check either mechanism.

## Front-end assets

Login uses `/limitless` and `/global_assets` and disables the Zend layout. The desk uses `application/layouts/default.phtml`. That file's title is `{AccreditationName} - Permits` and it links to `/walkins` as "Permits Area". `layout.phtml` titles itself "Footprint - SA Rugby Tournament Management Portal" and is not selected by `LayoutPicker` while the module name is `default`.

`default.phtml` is very large (embedded data). It was not line-reviewed in full. The title and the permits link were confirmed.

`public/` also contains `archives`, `assets`, `assets_old`, `bootstrap2`, `dompdf`, `files`, `fonts`, `html`, `images`, `starters`, `wp-content`, `wp-includes`, and `FILESTO DOWNLOAD`. **Inferred** mixed leftovers. The Zend app does not bootstrap WordPress.

Two match-report PDFs under `public/archives/matchreports/` have names long enough that a Windows checkout without long-path support cannot create them. They are in git. They are not part of the permit flow.

## Development and testing

No README, no test suite, no seed data, no `.env`. Configuration is the ini file.

`IndexController::testAction` and `PermitsController::emailtestAction` are manual probes left in the controllers. They are not a test suite. Both are dangerous to run against production data: one reads a real identity number, the other sends mail.

Display of errors is forced on in the entry script, which is a poor fit for a public site.

## Deployment

`appspec.yml` (CodeDeploy, Linux):

| Hook | Script | Runs as |
|---|---|---|
| BeforeInstall | `scripts/install_dependencies` (`yum install -y httpd`) | root |
| ApplicationStop | `scripts/remove_archives.sh` | root |
| ApplicationStart | `scripts/start_server` (`service httpd start`) | root |

Files copy from `/` to `/mnt/data/www/html/mypass.zone/permits.mypass.zone/` and are owned by `apache:apache`.

`remove_archives.sh` deletes old CodeDeploy deployment directories under `/opt/codedeploy-agent/deployment-root/`, keeping the last successful install. It does not delete `public/archives`.

`scripts/stop_server` is not hooked. Its `isExistApp = \`pgrep httpd\`` assignment is invalid bash.

`bitbucket-pipelines.yml` uses image `python:3.5.7`. On branches `qa` and `prod` it installs `zip` and `boto3==1.3.0`, zips the tree to `/tmp/artifact.zip`, and runs `codedeploy_deploy.py`. That script uploads the zip to S3 and creates a CodeDeploy deployment. Bucket, application name, deployment group, region, and AWS keys come from pipeline variables (`QA_*` or `PROD_*`). The script waits for the deployment result.

`master` is not a pipeline branch. Remote heads are `master`, `prod`, and `ui-changes`. There is no `qa` branch. Whether `prod` has run this pipeline is **Unknown / Requires Confirmation**.

The last commit (`16d1793`, 2022-07-01) only adds these deploy files.

## Security notes for reviewers

These are properties of the code, not a recommended attack.

- Database and SMTP passwords and the shared PIN are in the repository.
- `/login/external` sets the event id from the query string.
- Company passkeys are rendered into the company-list HTML.
- Search and several `fetchRow` calls concatenate request strings into SQL.
- Passwords are `md5` without a salt. Reset email includes the new password.
- `uploadremotecompanyAction` writes an uploaded image with no session check.
- `display_errors` is on. `updateData` prints the row it is about to save.
- `emailtestAction` and `testAction` are reachable as controller actions if the session gate is met (`emailtest` needs `EID`; `test` is behind the redirect).

No rate limit, captcha, or row-level security layer was found. Apache is installed and started by the deploy hooks. No TLS configuration is in this repository. **Unknown / Requires Confirmation** whether TLS is terminated in front of this host.
