# Technical documentation — bluecard.footprintapp.net

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/bluecard.footprintapp.net.git`. Deployed behaviour is `origin/prod` `aa2cc42` (2022-10-19). `origin/qa` `a5caf1a` differs only in the database host. `origin/master` `3e123e6` is the 10 March 2022 import.

Provenance: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

Credentials that appear in tracked config and source are not copied into this file.

## Architecture

**Implemented.** Classic Zend Framework 1 MVC. On prod there is one module.

1. Root `.htaccess` rewrites any URI that does not already start with `/public/` to `public/{path}`. HTTPS and `www` redirects in that file are commented out.
2. `public/.htaccess` leaves real files and directories alone and sends other requests to `index.php`.
3. `public/index.php` enables `display_errors` and `E_ALL|E_STRICT`, sets the time zone to `Africa/Johannesburg`, defines `APPLICATION_PATH` as `../application`, defaults `APPLICATION_ENV` to `production` unless the environment variable is set, defines `UPDATE_PATH` as `public/archives`, puts `library/` on the include path, and runs `Zend_Application`.
4. A maintenance splash at the top of `public/index.php` is commented out, including the `exit`.
5. `application/configs/application.ini` sets the module directory, PDO MySQL, layout `default`, session cookie lifetime `864000`, locale `af_ZA`, and front-controller plugin `PG_Controller_Plugin_LayoutPicker`.
6. `LayoutPicker::preDispatch` sets the layout name to the request module name. On prod that is `default`.
7. `Bootstrap::_initDbAdapter` is empty. The adapter comes from the ini resource.
8. The root `index.php` is an empty file (0 bytes). It is not the front controller.

Routing is the Zend default: `/` is `default/index/index`, `/index/submit` is `submitAction`, `/index/test` is `testAction`. No custom routes file was found.

## Application structure (prod)

```text
public/index.php                         front controller
public/.htaccess                         Zend rewrite
.htaccess                                rewrite into public/
application/configs/application.ini      env and database
application/Bootstrap.php                empty DB init
application/layouts/default.phtml        only layout on prod
application/models/Bluecard.php          BlueCard_Consents, union emails
application/models/Unions.php            ef_unions dropdown
application/models/Ethnicity.php         Ethnicity dropdown
application/models/*.php                 other Footprint models, unused by this module
application/modules/default/controllers/IndexController.php
application/modules/default/controllers/ErrorController.php
application/modules/default/views/scripts/index/index.phtml
application/modules/default/views/scripts/index/test.phtml
library/Zend/                            Framework 1.11.10
library/PG/                              LayoutPicker and other plugins
library/Other/                           present; not called by IndexController
public/archives/bluecard/                mail attachments
public/archives/bluecarddownload/        thank-you download links
public/wp-content, public/wp-includes    static theme assets
public/assets, public/bootstrap2         steps, validate, datepicker, bootstrap
appspec.yml, bitbucket-pipelines.yml, codedeploy_deploy.py
scripts/                                 CodeDeploy hooks
```

Prod tracks 10,760 paths. 7,809 are under `public/`. `public/archives` is 23 files.

`master` also has `website/` (26,740 paths), `test/` probe pages, and modules `bluecard`, `menu`, `companiesold`, and `companyonl`. Those are not in the prod root or prod module directory.

`application/modules/default/views/scripts/index/test.phtml` is a 65-line HTML fragment, about 230 KB because one line embeds a base64 PNG. It repeats the email placeholders (`[System Generated Date]`, "Dear [Logged User or Nominated other]"). `testAction` never renders it: the action builds the body in PHP and calls `exit`. **Inferred** leftover template.

View helpers `Controller.php`, `Efiling.php`, `Userdetails.php`, and `image.php` are in the module. `IndexController` does not call them.

## Framework and language

| Item | Value | Evidence |
|---|---|---|
| Language | PHP | `public/index.php`, controllers |
| Framework | Zend Framework 1.11.10 | `library/Zend/Version.php` |
| UI | Server-rendered phtml, jQuery Steps, jQuery Validate | `index.phtml`, `default.phtml` |
| Database access | `Zend_Db_Table`, adapter `pdo_mysql` | models, `application.ini` |
| Mail | `Zend_Mail` and `Zend_Mail_Transport_Smtp` | `sendMail` |
| PHP version | **Unknown** | `mcrypt_create_iv` in `random_id` requires the old `mcrypt` extension |

`IndexController` sets `memory_limit` to `180M` and `error_reporting` to `E_ALL` without `E_WARNING`, after the front controller has set `E_ALL|E_STRICT`.

## Database

**Implemented** connection, from `application.ini` production section:

| Setting | Prod | QA |
|---|---|---|
| Host | `db.footprintapp.net` | `qadb.footprintapp.net` |
| Database name | `teamwizz_SARU` | `teamwizz_SARU` |
| Adapter | `pdo_mysql` | same |

Username and password are in the same file and are not reproduced here. `staging`, `testing`, and `development` sections inherit production. Nothing in the repo selects those section names. `public/index.php` defaults the environment to `production`.

`.gitignore` on prod contains the line `application/configs/application.ini`. The file is nevertheless in the prod commit (`git show origin/prod:application/configs/application.ini` returns it). Ignore rules do not remove a file that is already tracked.

Tables used by the live module:

| Table | Use | Model |
|---|---|---|
| `BlueCard_Consents` | `insert($data)` of the posted array. `$_name` on the model | `Model_Bluecard` |
| `ef_unions` | `un_id`, `un_name` where `un_type = 3` and `main = 1` | `Model_Unions` |
| `profile` | `UserID`, `FirstName`, `LastName`, `Email` for BokSmart reps | `getUnionsEmails` (integrity check off, so the select is not limited to `BlueCard_Consents`) |
| `Ethnicity` | all columns, filtered to `EthCountry = 'South Africa'` | `Model_Ethnicity` |

No migration, schema dump, or seed is in the repository. Column names for `BlueCard_Consents` are **Unknown**. `updateData` writes `AC = '{id}'` and has no caller.

`getUnionsEmails` concatenates `UnionName` into the where clause.

## Auth and session

**Implemented.** No login controller on prod. No `Zend_Auth` use in the default module. Session cookie lifetime is configured and unused by this flow.

`ErrorController` returns 404 for a missing route, controller, or action ("Page not found") and 500 otherwise ("Application error"). It logs only if a Log bootstrap resource exists. `Bootstrap` does not register one. `displayExceptions` is 0 in the ini, and the error view receives the exception only when that flag is true. This is separate from `display_errors` in `public/index.php`, which still prints PHP errors that are not caught by the error controller.

## Mail

**Implemented** in `IndexController::sendMail`.

- Transport: `smtp` host `mail.sarugby.co.za`, port 25, auth `login`, username `bluecard@sarugby.co.za`. Password is in the method and is not reproduced here.
- Commented blocks show an earlier Office 365 setup (`smtp.office365.com`, TLS, port 587) and a relay host `sarugby-co-za.mail.protection.outlook.com`. They are not active.
- From: `bluecard@sarugby.co.za`, name "Bluecard at sarugby.co.za".
- HTML body. Attachments are `Zend_Mime_Part` with `DISPOSITION_INLINE` and base64.
- CC is `addCc` of the whole list when the list is non-empty.
- Exceptions are printed with `print_r` and not rethrown.

`testAction` calls the same method with a hardcoded recipient and sample names, then exits. It does not insert a row. It attaches six files; submit attaches seven.

## Front-end behaviour worth not missing

**Implemented** in `index.phtml`.

- Wizard: jQuery Steps, vertical orientation, finish label "Agree & Submit".
- Submit: `$.post("/index/submit", $("#BlueCardForm").serialize(), success)`. No error handler. A module-level `submittd` flag blocks a second post.
- Player role removes step index 2 and shows `#player_area`. Switching away reinserts the step from a saved HTML string.
- Contacts: string-replace of `data[Contact][0][...]` to the next index, capped at four `.row` nodes.
- Thank-you HTML is rendered into `#ThankYouHidden` on first paint and copied over `.content` after the post returns.

Absolute URLs inside emails and the thank-you string are `https://{SERVER_NAME}/`. `4e9ba84` changed the controller to that scheme. `default.phtml` still requests Google Fonts over `http://fonts.googleapis.com`.

The layout includes `https://cdn.ywxi.net/js/1.js`. The repository does not say what that script is.

## Deployment

**Implemented.**

`bitbucket-pipelines.yml` image is `python:3.5.7`. Branches `qa` and `prod` each:

1. Export AWS keys, region, `APPLICATION_NAME`, `DEPLOYMENT_GROUP_NAME`, and `S3_BUCKET` from Bitbucket variables prefixed `QA_` or `PROD_`.
2. `apt-get install zip`, `pip install boto3==1.3.0`.
3. `zip -r /tmp/artifact.zip *`.
4. `python codedeploy_deploy.py`.

`codedeploy_deploy.py` uploads that zip to `s3://{S3_BUCKET}/{APPLICATION_NAME}/{timestamp}-bitbucket_builds.zip` and calls `create_deployment`. `deploymentConfigName` is `str(os.getenv('DEPLOYMENT_CONFIG'))`. The yaml does not export `DEPLOYMENT_CONFIG`. If the variable is unset, the API receives the string `None`. Whether Bitbucket repository variables supply it is **Unknown / Requires Confirmation**.

The script then polls `get_deployment` until `Succeeded`, `Failed`, or `Stopped`. `sleep` is imported and not called, so the poll is a tight loop. `ignoreApplicationStopFailures` is true. Upload or deploy failure exits 1.

`appspec.yml`:

- OS `linux`.
- Files from `/` to `/mnt/data/www/html/footprintapp.net/bluecard.footprintapp.net/`.
- Owner and group `apache`.
- BeforeInstall: `scripts/install_dependencies` (`yum install -y httpd`), 300s, root.
- ApplicationStop: `scripts/remove_archives.sh`, 300s, root. Despite the name, the script deletes old CodeDeploy revision directories under `/opt/codedeploy-agent/deployment-root/`, excluding `deployment-instructions` and the last successful install. It does not delete `public/archives`.
- ApplicationStart: `scripts/start_server` (`service httpd start`), 300s, root.

`scripts/stop_server` is not referenced. Its `isExistApp = \`pgrep httpd\`` assignment is not valid bash (spaces around `=`).

No PHP package, vhost, or `APPLICATION_ENV` export is in the hooks. **Inferred:** the instance already has PHP and a vhost pointing at this directory. **Requires Confirmation.**

**Inferred risk:** `zip *` from a default shell omits dotfiles, so root `.htaccess` and `public/.htaccess` may be absent from the artifact. Both are required for routing. **Requires Confirmation** against a real artifact.

`master` is not a pipeline branch.

## Environments

| Name | How it is selected | What differs |
|---|---|---|
| production | Default in `public/index.php` | Database host `db.footprintapp.net` |
| The `qa` branch | Separate deploy, same PHP default of production | Ini host `qadb.footprintapp.net` only |
| staging, testing, development | Ini sections exist and inherit production | Not selected by any file that was read |

No `.env` file. No separate prod mail host. QA uses the same `sendMail` code, so a QA deploy sends through `mail.sarugby.co.za` the same way. **Implemented.**

## Background work, queues, storage

No cron, queue, or worker is registered on prod. `CronController` exists only on `master`.

File storage is the git tree itself: PDFs and spreadsheets under `public/archives/bluecard/` and `public/archives/bluecarddownload/`. `UPDATE_PATH` points at `public/archives`. There is no S3 upload from the PHP application. S3 is used only as the CodeDeploy revision bucket.

`public/dompdf` is on prod and is not used by `IndexController`.

## Monitoring and logging

No application log resource, metrics hook, or health endpoint was found. `ErrorController` will log if a Log resource exists; it does not. Mail and SQL failures are written to the HTTP response.

The layout loads a third-party script from `cdn.ywxi.net`. No analytics snippet with a tracking id was found in `default.phtml`.

## Security notes (evidence, not a pentest)

These are properties of the code. They are listed so a reviewer can see them. Offsets, payloads, and secret values are omitted on purpose.

- Database password and SMTP password are committed. `.gitignore` names the ini and does not remove the tracked copy.
- `/index/submit` and `/index/test` are unauthenticated. `testAction` sends real mail.
- `getUnionsEmails` builds SQL by concatenating the posted union name.
- `display_errors` is on, and exceptions are printed to the client.
- No CSRF token was found on the form.
- `random_id` depends on `mcrypt`.

## Development and testing

No README, PHPUnit config, Composer file, or application test suite is on prod. The `test/` directory on `master` is a set of hosting probe pages (`test.php`, FastCGI, SSI), not tests of the form.

The Bitbucket pipeline deploys. It does not run tests.

`sonarqube_scan.bat` and `sonar-project.properties` are not in the git root of `master`, `qa`, `prod`, or `bluecard-11-03-2022`. The onboarding task listed them in a working-tree scan. **Unknown** whether they were untracked local files.

## Branch map

```text
dc816bf  Initial commit (parent)
3e123e6  Initial commit          origin/master, origin/HEAD
85d79d4  Moved bluecard into top level domain
... feature commits on bluecard-11-03-2022 ...
f990a10  Added additional fields for players     origin/bluecard-11-03-2022
a5caf1a  Merge into qa                           origin/qa
aa2cc42  Merge qa into prod                      origin/prod
```

A full `git diff --stat origin/qa origin/prod` is one line: the database host in `application.ini`.
