# Technical documentation — theear (The Ear)

Evidence date: 2026-10-01. Source: `git@bitbucket.org:overdrive/theear.git`, `master` at `f1a82c841` (2023-08-10). Shallow history shows 94 commits from `c51c17f` (2023-03-03) through that tip. Older history was not fetched.

## Stack

| Item | Evidence |
|---|---|
| PHP | `composer.json` requires `>=5.3.7`. Production version is **Unknown**. Libraries such as mPDF 8 and Google2FA 8 need a newer PHP than 5.3 in practice. **Requires Confirmation** of the server version |
| Framework | CodeIgniter 3.1.9 (`system/core/CodeIgniter.php` `CI_VERSION`) |
| Database | MySQL, driver `mysqli`, UTF-8 (`application/config/database.php`) |
| Web server | nginx, docroot `/usr/share/nginx/html` (`appspec.yml`). `.htaccess` rewrite is Apache syntax. How nginx maps that rewrite is **Unknown** (not in the repo) |
| Front controller | `index.php`. `ENVIRONMENT` from `CI_ENV`, default `production` |
| Sessions | `sess_driver` database (`config.php`) |
| PDF | `mpdf/mpdf` ^8 |
| Mail | PHPMailer ^6.5, SMTP settings via AWS parameters |
| 2FA | `pragmarx/google2fa`, QR via bacon and chillerlan |

`config['base_url']` is forced to `https` plus `HTTP_HOST` plus the script directory. `encryption_key` is empty.

## Repository layout

| Path | Role |
|---|---|
| `application/` | Controllers, models, views, config, helpers, libraries |
| `system/` | CodeIgniter core |
| `vendor/` | Composer packages, committed in the tree (8614 files in the clone, mostly vendor and assets) |
| `assets/`, `static/`, `uploads/` | Front-end assets and uploads |
| `db_updates/` | Hand-written SQL deltas (48 `.sql` files in the listing) |
| `documentation/` | PDF manuals. AppleDouble `._*` siblings are also present |
| `research/MIDI.js-master/` | Vendored MIDI.js |
| `pdf/`, `rich/`, `update/`, `update_pack/` | Present at the root. Contents were not inventoried file by file |
| `bitbucket-pipelines.yml` | Active pipeline definition |
| `bitbucket-pipelines-ftp.yml` | Alternate FTP pipeline file, not the default name |
| `appspec.yml` | CodeDeploy file mapping |
| `sonar-scanner.bat` | Present. `sonar-project.properties` is not in this commit |

`.gitignore` ignores `node_modules/`, `dist/`, logs, and IDE files. It does not ignore `vendor/`.

## Application structure

Controllers on `master`:

- `Home.php` — public site, cart, PayFast, lessons, playgrounds. `Home_old.php` and `Home_olda.php` are previous copies.
- `Login.php` — credentials, MFA, registration, reset.
- `Admin.php` — back office.
- `User.php` — instructor area.
- `Api.php`, `Api_instructor.php` — JSON.
- `Cron.php` — `updateCurrencies`, `GenerateInvoicePDF` only. The large job set is not in this class.
- `Feedback.php` — create, upload, set status, set note.
- `Modal.php` — popup partials.
- `Install.php`, `Updater.php` — Academy setup.
- `Checker.php` — school-style API, domain does not match The Ear.
- `addons/Bundle.php`, `Course_bundles.php`, `Course_analytics.php`.

Models: `Crud_model` (very large), `User_model`, `Hearing_model`, `Payment_model`, `Email_model`, `Email_tracking_model`, `Institution_model`, `Api_model`, `Api_instructor_model`, `Cron_model`, `Reports_model`, `Ticket_model`, `Video_model`, `Addon_model`, `Jwt_model`, `Lazyloaddata_model`, plus addon models for analytics and bundles.

Helpers: `common_helper.php` (permissions, settings, S3 presign), `user_helper.php`, `addon_helper.php`, `multi_language_helper.php`, `pagination_helper.php`.

Libraries: `REST_Controller.php`, `TokenHandler.php`, `JWT.php`, `Paystack.php`, `Format.php`.

Views: `frontend/`, `backend/`, `email/`, `payment/`, `lessons/`, `mobile/`, `install/`, `errors/`, `bundle_payment/`, `static/`.

Routes (`application/config/routes.php`): default `home`, 404 `home/page_not_found`, certificate and course-bundle aliases. Everything else is CodeIgniter's class/method convention.

Hooks file has no enabled hooks.

## Configuration and secrets

`get_settings` (`common_helper.php`) special-cases these keys and does not read them from the `settings` table:

| Key | Source |
|---|---|
| `amazon_s3_access_key` | `aws_get_secret("secret_access_key", "Access_key_ID")` |
| `amazon_s3_secret_key` | `aws_get_secret("secret_access_key", "Secret_access_key")` |
| `access_region_default` | `aws_get_parameter` |
| `smtp_host`, `smtp_user`, `smtp_port` | `aws_get_parameter` |
| `smtp_pass` | `aws_get_secret("ses_smtp_password")` |
| `xero_client_id`, `xero_code_verifier`, `xero_code_challenge`, `xero_tenant_id` | `aws_get_parameter` |

Any other key is `settings.value` for that `settings.key`.

Database: `aws_get_parameter("db_host")`, `aws_get_parameter("db_username")`, `aws_get_secret("db_password")`, `aws_get_parameter("db_table")`.

`function aws_get_parameter` and `function aws_get_secret` are **not defined** anywhere in the PHP tree outside `vendor` (search excluded vendor and system). The app cannot boot those calls unless the functions are injected on the server (auto_prepend, an uncommitted file, or an extension). Provenance: **Unknown / Requires Confirmation**.

`database.php` contains commented credentials for `medmin.co.za` and for local database `ear`. They are not active. They should be treated as leftover secrets in git.

PayFast merchant id, passphrase, and `test_mode` are read from setting `payfast_keys` (JSON). Evidence: `cron_daily.php`.

## Data

There is no full schema dump. Tables are known only where SQL or PHP names them. Base Academy tables (`users`, `role`, `course`, `section`, `lesson`, `enrol`, `payment`, `category`, `settings`, `permissions`, `payout`, `currency`, and others) are used and are not created in `db_updates`. Their original definitions are **not in this repo**.

Tables created in `db_updates` (names only):

| Table | Script |
|---|---|
| `instrument_level`, `payment_terms`, `coupon_type`, `instrument`, `email_templates` | `update.1.rachel.sql` |
| `resources` | `update.1.liam.sql` |
| `hearing_content`, `tracking` | `update.6.liam.sql` |
| `earmarks` | `update.8.liam.sql` |
| `ticket` | `update.9.jaco.sql` |
| `note` | `update.10.maanie.sql` |
| `mailing_list` | `update.11.maanie.sql` |
| `certificates` | `update.15.rachel.sql` |
| `payfasttransactionshistory` | `update.16.rachel.sql` |
| `hearing_tracking` | `update.17.liam.sql` |
| `syslogin` | `update.17.rachel.sql` |
| `credit_notes`, `email_template_images` | `update.18.rachel.sql` |
| `email_tracking` | `update.36.liam.sql` |
| `affiliates` | `update.28.liam.sql` (schema name in the script: `the_ear_staging_database`) |
| `affiliate_payouts` | `update.29.liam.sql` (schema name: `prod_test`) |
| `institutions` | `update.37.liam.sql` (schema name: `prod_22_aug_22`) |

`syscronjob` is written by the cron views and is not created in the SQL files that were grepped for `CREATE TABLE`. Its DDL is **Unknown** (likely applied outside these files, or in a file that uses `ALTER` only).

Role rows added by SQL: `Tester` with a NULL id (`update.24.liam.sql`); `Institution User` with id 4 (`update.38.liam.sql`).

Engine mix: several Liam scripts use MyISAM; Rachel's instrument script uses InnoDB. Provenance: **Documented in SQL**.

## Auth

| Surface | Mechanism |
|---|---|
| Browser learner | Session `user_login`, password sha1, status and `is_disabled` |
| Browser admin | Session `admin_login`, optional TOTP, then `check_permission` |
| Learner API | JWT HS256, shared key in source, `rest_auth` FALSE |
| Instructor API | `Api_instructor::login_post` then token methods |
| Cron and Xero HTTP | Session check commented out |

Login audit: `syslogin` via `Crud_model::save_login`.

## Jobs

Two HTTP endpoints, not CLI workers:

- `admin/run_cron_daily/{cronjob}` → switch in `cron_daily.php`.
- `admin/run_cron_hourly` → drains `syscronjob` where `status` is `New`.

`Cron.php` is a small extra controller (currency, invoice PDF) and is not the daily switch.

No queue daemon (SQS, beanstalk, cron file) is in the repo. The scheduler is **Unknown**.

Xero: OAuth views `authorization.php` and `callback.php`, journal post in `authorizedResource.php` using `XeroAPI\XeroPHP\Api\AccountingApi::createManualJournals` and tenant id from `get_settings('xero_tenant_id')`. Both Calcinai Xero PHP and the official OAuth2 SDK are in Composer.

## Storage and media

`get_signed_url` builds an S3 presigned GetObject for 60 minutes. Bucket name is setting `amazon_s3_bucket_name`. Region setting `amazon_s3_region_name`.

Instrument landing methods assign `s3://the-ear-prod-transcode/transcode/MAIN_FOLDER/Walkthroughs/eAr_WALKTHROUGH_01/eAr_WALKTHROUGH_01.m3u8.m3u8`.

`Admin::createTheInvalidation` calls the AWS CloudFront client. Distribution id is in that method (not copied here).

Uploads land under `uploads/` (user images, thumbnails, system logos referenced by `Api::app_logo_get`).

## Deployment

`bitbucket-pipelines.yml`:

- Image `atlassian/default-image:2`.
- Branch `master`: zip the working tree to `application1.zip`, CodeDeploy upload then deploy, wait true, version label `my-app-1.0.0`, `FILE_EXISTS_BEHAVIOR` OVERWRITE, ignore application-stop failures. Credentials: `$CODEDEPLOY_ID_STAGING` and `$CODEDEPLOY_KEY_STAGING`.
- Branch `production`: same steps with `$AWS_ACCESS_KEY_ID_PROD`, `$AWS_SECRET_ACCESS_KEY_PROD`, `$APPLICATION_NAME_PROD`, `$S3_BUCKET_PRODUCTION`.

`appspec.yml` version 0.0, os linux, source `/` to `/usr/share/nginx/html`, owner and group `nginx`, mode 755.

The zip includes the whole directory (`.`). That includes `vendor`, `uploads`, and `research` unless ignored by the zip command. The script zips `.` with no exclude list.

No test step runs in the pipeline.

## Development and testing

No `tests/` directory. `composer.json` require-dev lists PHPUnit 4 or 5, which matches upstream CodeIgniter and is unused here.

No README. PDFs: `documentation/Administrator Usage Guide .pdf` (space before `.pdf`), `Developer Manual.pdf`, `Installation-and-update.pdf`, `Quick-Start-Guide.pdf`, `User Usage Guide.pdf`.

Sonar scanner batch file is present. Project properties file is not. `.scannerwork` appeared in the working clone listing; it is a scanner output directory and was not treated as product source.

## Security notes (observed, not a pentest)

- SHA-1 passwords, empty encryption key, JWT secret in source.
- Cron and Xero controllers skip the admin session check.
- REST authentication disabled at the library config.
- Commented database password remains in `database.php`.
- `Checker.php` and `Install.php` are additional entry points if the web root is this tree.

## Not established

- Production hostname, PHP version, nginx vhost, and cron user.
- Definition and AWS account of `aws_get_parameter` / `aws_get_secret`.
- Full table list and which `db_updates` scripts have been applied.
- Whether branch `production` still exists on the remote (this fetch showed only `master`).
- Text of the PDF manuals.
