# Technical documentation — snell-freestock-5.6 (Freestock Requisition)

Evidence date: 2026-10-02. Source: `main` at `812337f` (2025-06-13, "filters fixes").

## Stack

| Area | What the tree shows |
|---|---|
| Language | PHP. Classes use PHP 4-style constructors (`function odbc`, `function sqlObject`) and `var` properties. `qs()` calls `get_magic_quotes_gpc()`, which PHP removed in 8.0 and deprecated earlier. **Inferred** target is PHP 5.x. The repository name `5.6` is consistent with PHP 5.6 and is **not confirmed** |
| Web server | IIS. `web.config` sets the default document to `index.php`. Session paths use Windows backslashes |
| Database | Microsoft SQL Server through PHP ODBC. `sqlObject` reads `sys.sysobjects`, `sys.syscolumns`, and `sys.systypes` |
| Browser script | jQuery 1.4.2 (loaded by the header). Also present and unused by the header: jQuery 2.1.4, jquery-impromptu 3.1, tablesorter (loaded), a scrolling-table plugin, md5.js, menu.js |
| Auth library | Vendored adLDAP under `ldap/`. Not constructed |
| Package manager | None. No `composer.json`, `package.json`, or lockfile |
| Tests | None. `test.php` is a manual diagnostic page |
| CI | None in git |

## Application structure

```text
index.php                  redirect to login.php
login.php                  IE check, LogginUser, login layout
logout.php                 session_destroy when Action=Logout
requisition.php            list
requisitionForm.php        form and actions (live)
requisitionFormold.php     superseded form
user.php                   user admin
ajax.functions.php         XML lookups
test.php                   diagnostic
web.config                 IIS default document
include/
  system.inc.php           connection, session, version, ceiling
  system.functions.inc.php LogginUser, filters, helpers
  requisition.cls.php      list, save, workflow
  user.cls.php             user list and save
  layout.*.inc.php         header, menu, footer, full page, login page
  database/odbc.cls.php    ODBC wrapper
  database/sql_object.cls.php   column discovery and insert/update
ldap/                      adLDAP (dormant)
```

Page flow for an authenticated screen: the page includes `system.inc.php`, handles `Action`, builds `$CONTENT`, then includes `layout.full.inc.php`. That layout sends the browser to `login.php` when `$_SESSION[USER]` is missing, then prints header, menu, message, a single POST form, and footer.

`layout.footer.inc.php` includes `system.inc.php` again (plain `include`, not `include_once`). The footer string is version, an optional `[Dev]` tag when the host is `sql-server`, and `$FOOTERDATE`.

Global request extraction is not a framework. Pages read `$Action`, `$UserID`, `$RequisitionID`, and `$ItemID` as bare variables. `system.functions.inc.php` defines `gpc_extract` but the bootstrap does not show a `register_globals` call. Whether those bare variables are populated by a removed extractor, by register_globals, or only when the form posts names that PHP still imports is **Unknown / Requires Confirmation**. The form does post `Action`, `UserID`, `RequisitionID`, and `ItemID`.

## Runtime configuration

All of this is in `include/system.inc.php`. There is no `.env` file.

| Host `SERVER_NAME` | ODBC | After connect | Logon override |
|---|---|---|---|
| `sql-server` | DSN `snell_fsr` | Does not call `setDatabase`. A commented line would set database `snell_2015` | `LOGON_USER` forced to `silicon\pj` |
| anything else | DSN `snell_fsr`, then `setDatabase("snell_2015")` which opens a second connection whose DSN name is `snell_2015` | User and password are the same as the first connection | none in this file |

The ODBC user name is `snell_fsr`. The password is a literal in `include/system.inc.php`. It is not copied into this document. A commented alternative DSN is `snell_fsr_access` with empty credentials.

`LogginUser` then replaces the logon name with `AzureAD\UrielBalela` when that override variable is not null. See BR-002.

Session:

- `session.gc_maxlifetime` = 21600
- Save path when host is `sql-server` or `localhost`: `{DOCUMENT_ROOT}\snell_freestock_PHP\Sessions`
- Otherwise: `{DOCUMENT_ROOT}\snell_freestock\Sessions`
- `session_start()` in the bootstrap
- `SCRIPT_NAME` has the prefix `/snell_freestock_PHP/` stripped

Version constants: `$VERSIONNUM = "2.0.1s"`, `$FOOTERDATE = "31 May 2016"`, `$SYSTEM = "Freestock Requisition"`, `$SALESMANAGERMAXVALUE = 10000`.

## Data touched by this application

There is no `CREATE TABLE` in the repository. Columns below are the ones named in SQL. Other columns exist: `sqlObject` discovers them from `sys.syscolumns` and will insert or update posted names that match.

| Object | Role | Columns named in code |
|---|---|---|
| `mstUser` | People | `UserID`, `strUserName`, `strDomainLogin`, `strEmail`, `strRegion`, `RepID`, `blnActive`, `blnManager`, `blnManagerExecutive`, `refUserGroupFreeStockID`, `refManagerID`, `refManagerAlternativeID`, `refManagerExecutiveID`, `strLastUser`, `dtStamp`. `blnSpecial` is read from the login `SELECT *` and is not named in an `mstUser` column list |
| `mstUserGroupFreeStock` | Groups | `UserGroupID`, `strUserGroup` |
| `fsrRequisition` | Header | `RequisitionID`, `refUserID`, `refRequestTypeID`, `strPinkSlipNumber`, `dtDate`, `strRegion`, `strExpense`, `strBillToAccount`, `strShipToAccount`, `txtRequestDetail`, `strContactPerson`, `strContactNumber`, `blnDelivery`, `strStatus`, `strAuthorizedBy`, `strInvoiceNumber`, `refManagerID`, `refAlternateManagerID`, `refExecutiveManagerID`, `dtLastEdit`, `dtEmailSentToAltManager`, `UniqueID` (unset before save) |
| `fsrItem` | Lines | `ItemID`, `refRequisitionID`, `strBrand`, `strProdCode`, `intQuantityCase`, `dblCostCase`, `intQuantityBottle`, `dblCostBottle`, `dblTotal`, `strFirstUser`, `dtFirstEdit` |
| `fsrRequestType` | Lookup | `RequestTypeID`, `strRequestType`, `blnActive` |
| `mstBranch` | Regions | `strRegionDesc2` |
| `mstBrand` | Brands | `strBrand`, `blnActive` |
| `dwdCustomer` | Accounts | `strCustomerNo`, `strCustomerName`, `strRegionDesc2`, `blnActive`, `blnPromotionalAccounts` |
| `dwdProduct` | Products and prices | `strProdCode`, `strLongProdDesc`, `strBrand`, `dblPriceCase`, `dblPriceBottle`, `blnActive` |
| `mstUsageLog` | Login audit | `mstUserID`, `dtStamp`, `strSystem` |
| `sysCronJob` | Outbound jobs | `dtCronJob`, `Type`, `param`, `Status`, `Result` |
| `vieRequsitionManager` | Manager emails. The view name is spelled Requsition | `RequisitionID`, `ManagerName`, `ManagerEmail`, `AltManagerName`, `AltManagerEmail`, `ExeManagerName`, `ExecutiveManagerEmail` |
| `sysQuery` | Counted by `test.php` only | none named |

`odbc::getLastID` calls `odbc_insert_id()`, which is not a standard PHP ODBC function (that API belongs to a MySQL-style extension). Whether pink-slip generation and new user ids work on the deployed driver is **Unknown / Requires Confirmation**. The pink-slip `UPDATE` uses the id returned by that call.

## Auth

1. IIS is expected to set `LOGON_USER`. This repository does not configure Windows authentication; `web.config` only sets the default document and an empty rewrite map whose name is `http://localhost/snell_freestock/requisition.php`.
2. `LogginUser` matches that string to `mstUser.strDomainLogin` after the overrides in the runtime section.
3. The session holds the whole user row on `$_SESSION["USER"]`.
4. `layout.full.inc.php` is the only session gate. Login, logout, `test.php`, and `ajax.functions.php` do not use it.
5. `checkLogin` (IP match) has no caller.

No password hashing, CSRF token, or API key is present.

## Background work

This process does not run a worker, queue client, or scheduler. It inserts and updates `sysCronJob`. Types written here: `FSR_NewRequisition`, `FSR_RequisitionApproved`. Status values written here: `New`, `Removed`. The consumer is **Unknown**.

## Integrations

Documented in `prd.md`. Short form: ODBC to SQL Server, IIS logon in, `mailto:` links out, cron rows out. adLDAP is on disk and not called.

## Security notes (implemented behaviour)

These are observations for people maintaining the system. They are not an attack procedure.

- Database credentials are hardcoded in `include/system.inc.php`.
- A fixed directory username is hardcoded in `LogginUser` and marked not for production.
- SQL is built by concatenating request values (`frSearch`, account ids, product codes, sort field names from the query string). `qs()` wraps values in quotes and strips magic quotes. It is not a parameterised query.
- `ajax.functions.php` runs those lookups without the layout session check.
- `user.php` does not check `blnSpecial`.
- Sort links pass `sortCurrentField` into `ORDER BY`.

## Development and testing

No PHPUnit, no seed data, no Docker file, no pipeline. Local diagnosis that exists in-tree is `test.php` (counts `sysQuery`, prints encoding samples, reloads ship-to accounts for region `KZN`).

The Bitbucket history is five commits in June 2025 by Uriel Balela. Comments name earlier authors: Stephen (2010), Yusiry (2015), pj (2015–2016), maanie (2016–2024).

## Human confirmation

1. What the repository name `5.6` means, against footer version `2.0.1s` dated 31 May 2016.
2. Whether the `AzureAD\UrielBalela` override in `LogginUser` is still present on the running site.
3. Production host name, IIS site path (`snell_freestock` vs `snell_freestock_PHP`), and which ODBC DSN is live (`snell_fsr` vs `snell_2015`).
4. Whether `odbc_insert_id()` returns the new `RequisitionID` on that driver. Pink slips depend on it.
5. Which table and column hold `blnSpecial`, and the real names in `mstUserGroupFreeStock` (only id 6 is mentioned, as Executive).
6. The program that reads `sysCronJob`, and the program that sets status `Completed` and `strInvoiceNumber`.
7. Whether special users are allowed to approve above R10 000. The code does not apply the ceiling to them.
8. Whether `user.php` should refuse users who are not `blnSpecial = 1`.
9. Whether `sonar-project.properties` and `sonarqube_scan.bat` exist outside git. They were listed at task creation and are not in `main`.
10. How bare `$Action` and `$RequisitionID` are populated if `register_globals` is off.
