# Technical documentation — snell-freestock (Freestock Requisition)

Evidence date: 2026-10-02. Source: `master` at `477f559` (2026-09-18), `git@bitbucket.org:overdrive/snell-freestock.git`.

There is no README, dependency manifest, or deployment guide in the repository. Facts below come from the tree that is loaded by the pages.

## Stack

| Item | Evidence | Status |
|---|---|---|
| PHP pages and includes | `.php` files at the root and under `include/` | Implemented |
| PHP 8 work | Commits `38e5937` and `ea0bd6e` (2023-11-28), messages "php 8 upgardes" and "php upgrades" | The repo does not pin a version. Server version is **Unknown** |
| IIS | `web.config` default document `index.php` | Implemented |
| SQL Server via ODBC | `odbc_connect`, and `sys.sysobjects` / `sys.syscolumns` in `sqlObject` | Implemented |
| Browser script | Header loads jQuery 1.4.2 min, `scripts.js`, calendar control, tablesorter | Implemented |
| adLDAP  | `ldap/` tree, PHP LDAP extension required by that library | Library present. Live login does not construct it |
| Composer, npm, Docker, CI | No manifest or pipeline file | Not in the repository |

`include/javascript/` also contains `jquery-1.4.2.js`, `jquery-1.4.2.min.js`, `jquery-2.1.4.min.js`, and `jquery.tablesorter.min.js`. The header references `jquery-1.4.2.min.js` and `jquery.tablesorter.min.js`. Nothing in the header or the requisition form references jQuery 2.1.4.

## 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           authorisation list
requisitionForm.php       requisition form, permissions, page script
user.php                  user list and form
ajax.functions.php        XML lookups
test.php                  manual AJAX and encoding page
web.config                IIS default document
include/system.inc.php    connection, session, version constants
include/system.functions.inc.php
include/requisition.cls.php
include/requisition.cls.20210503.php   not included
include/user.cls.php
include/layout.*.inc.php
include/database/odbc.cls.php
include/database/sql_object.cls.php
include/javascript, include/style, include/images
ldap/                     vendored adLDAP
Sessions/                 committed PHP session files
```

Page flow for the three authenticated screens: the page includes `system.inc.php`, builds `$CONTENT` and sometimes `$MESSAGE`, then includes `layout.full.inc.php`. That layout includes the header, menu, and footer, then refuses to render if `$_SESSION[USER]` is missing.

`layout.footer.inc.php` includes `system.inc.php` again. The footer is pulled in by both layouts. `system.inc.php` is not wrapped in `include_once`, so the connection and `session_start` run a second time on those pages. The AJAX file sets `display_errors` to 0 before including the bootstrap so a startup notice does not break XML. Provenance: **Implemented**.

Request globals: if `$_POST` is non-empty, `localize` copies it onto `$GLOBALS` and sets `$_GET` to null. Otherwise GET and cookies are copied the same way. That is why a posted form cannot also read query-string ids unless they were copied into hidden fields. `PostedRequisitionID` exists because of that behaviour.

## Runtime configuration

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

| Host `SERVER_NAME` | Database | Identity |
|---|---|---|
| `sql-server` | ODBC DSN `snell_fsr`. `setDatabase` is not called | `LOGON_USER` forced to `silicon\pj` |
| anything else | Same DSN, then `setDatabase("snell_2015")`, which calls `odbc_connect` with the database name as the DSN argument | `LOGON_USER` left as IIS supplied it |

The ODBC user name in source is `snell_fsr`. A password is in the same call. It is not repeated in this document.

Commented alternatives in that file: DSN `snell_fsr_access`, and `setDatabase("snell_2015")` disabled on the `sql-server` branch. SMTP `ini_set` lines are commented (`192.168.100.1` and a note `196.31.219.114`). No `mail()` call was found.

Session:

- `session.gc_maxlifetime` = 21600
- Save path `DOCUMENT_ROOT\snell_freestock_PHP\Sessions` when the host is `sql-server` or `localhost`
- Otherwise `DOCUMENT_ROOT\snell_freestock\Sessions`
- `$_SERVER[SCRIPT_NAME]` has the prefix `/snell_freestock_PHP/` removed before it is used as a filter-session key

`web.config` names an empty rewrite map `http://localhost/snell_freestock/requisition.php`. No rewrite rules are defined. The live URL is **Unknown**.

Constants:

- `$VERSIONNUM = "2.0.1s"`
- `$FOOTERDATE = "31 May 2016"`
- `$SYSTEM = "Freestock Requisition"`
- `$SALESMANAGERMAXVALUE = 10000`

## Data access

`odbc` wraps `odbc_connect`, `odbc_exec`, `odbc_fetch_object`, and `odbc_result`. `doQuery` returns the ODBC result and does not die on failure (the `or die` is commented). `getLastID` calls `odbc_insert_id()`. Whether that function returns a SQL Server identity through this driver is **Unknown**. `Save` treats a missing new id as a failed create.

`sqlObject` reads column names from `sys.sysobjects` and `sys.syscolumns` for one user table, then builds insert and update SQL from `$_POST` keys that match those columns. `sqlObject::doQuery` dies on `odbc_exec` failure. The 2026-09-18 requisition save path avoids that method and uses `$db->doQuery` so a failed write can set `$blnSaveOK`.

`qs()` in `system.functions.inc.php` trims, maps a few punctuation characters to ASCII, strips slashes, and turns double quotes into single quotes. It does not wrap the value in quotes. Callers that need a quoted literal add quotes themselves. `odbc::qs` does add quotes and still calls `get_magic_quotes_gpc`, which PHP 8 removed. That method is a different function from the global `qs`.

## Tables and view named in SQL

No `CREATE TABLE` scripts are in the repository. Columns below are the ones the PHP reads or writes. Other columns can exist; `sqlObject` discovers them at runtime.

| Object | Use |
|---|---|
| `mstUser` | People. Login key `strDomainLogin`. Flags `blnActive`, `blnManager`, `blnManagerExecutive`. Links `refManagerID`, `refManagerAlternativeID`, `refManagerExecutiveID`, `refUserGroupFreeStockID`. Also `strUserName`, `strEmail`, `strRegion`, `RepID`, `strLastUser`, `dtStamp` |
| `mstUserGroupFreeStock` | `UserGroupID`, `strUserGroup`. Joined at login |
| `mstUsageLog` | `mstUserID`, `dtStamp`, `strSystem` (`FreeStock`) |
| `mstBranch` | Distinct `strRegionDesc2` for region dropdowns |
| `mstBrand` | `strBrand`, `blnActive` |
| `fsrRequisition` | Header. Identity used as `RequisitionID`. Pink slip, status, expense, region, bill-to, ship-to, contact, delivery, detail, dates, manager ids, `strAuthorizedBy`, `strInvoiceNumber`, `dtEmailSentToAltManager` |
| `fsrItem` | `ItemID`, `refRequisitionID`, brand, product code, case and bottle quantities and costs, `dblTotal`, `strFirstUser`, `dtFirstEdit` |
| `fsrRequestType` | `RequestTypeID`, `strRequestType`, `blnActive` |
| `dwdCustomer` | `strCustomerNo`, `strCustomerName`, `strRegionDesc2`, `blnActive`, `blnPromotionalAccounts` |
| `dwdProduct` | `strProdCode`, `strLongProdDesc`, `strBrand`, `dblPriceCase`, `dblPriceBottle`, `blnActive` |
| `vieRequsitionManager` | Spelling as in the SQL. Columns used: `ManagerName`, `ManagerEmail`, `AltManagerName`, `AltManagerEmail`, `ExeManagerName`, `ExecutiveManagerEmail` |
| `sysCronJob` | `dtCronJob`, `Type`, `param`, `Status`, `Result` |
| `sysQuery` | `SELECT COUNT(*)` from `test.php` only |

`Save` unsets `UniqueID`, `strInvoiceNumber`, and `dtEmailSentToAltManager` before `setRow`, so a normal requisition post does not write those three. On insert it also unsets the manager ids, pink-slip number, and `strAuthorizedBy`.

`blnSpecial` is read from `$_SESSION[USER]`, which is the `SELECT *` login row. The owning table is **Unknown**.

## Auth

1. IIS is expected to set `LOGON_USER`. The PHP form does not collect a password.
2. `login.php` calls `session_destroy()` before `system.inc.php` calls `session_start()`.
3. `LogginUser` stores the joined user row on `$_SESSION["USER"]`.
4. If `$OVERRIDE_USERNAME` were set, it would replace the Windows name. The only assignment in source is commented.
5. Later pages trust the session. They do not re-read `LOGON_USER` except the `Link` branch on the requisition form, which calls `LogginUser` again.

Menu logout passes `Action=Logout`. The logout page destroys the session only in that case.

## Requisition save and queue

`Requisition::PostedRequisitionID` accepts only a scalar that matches `^[0-9]+$` and is greater than 0.

Item posts are arrays keyed by item id. Keys must be numeric and unchanged by an int cast (`"01"` would fail). Negative keys insert. Non-negative keys update `fsrItem` where both `ItemID` and `refRequisitionID` match. `strFirstUser` on insert is `PHP: {user name}` with quotes doubled.

`Submit` and `Approve` refuse a zero item count before they touch `sysCronJob`. They delete any existing row of the same type and `param`, insert status `New`, then update the requisition status. The delete/insert/status statements are not wrapped in a transaction in this code. A failure part-way through is **Unknown** beyond each call returning its ODBC result unchecked on the success path.

`Decline` and `Cancel` update every `sysCronJob` row with that `param`, not only the FSR types.

No worker, scheduled task, or Windows service is in the repository. **Unknown** what reads `Type` `FSR_NewRequisition` and `FSR_RequisitionApproved`.

## HTTP surface

| URL | Methods that matter | Session |
|---|---|---|
| `index.php` | GET, redirects | Starts none itself |
| `login.php` | POST `Action=Login`, or implicit when `LOGON_USER` is set | New session after destroy |
| `logout.php` | GET or POST `Action=Logout` | Destroys on that action |
| `requisition.php` | POST Filter and Clear; GET `Action=SORT` | Required by layout |
| `requisitionForm.php` | POST Save, Submit, Approve, Decline, Cancel Requisition; GET open, `Link`, `RemoveItem` | Required by layout, after the optional `Link` login |
| `user.php` | POST Save, Make Inactive, Filter, Clear; GET Edit, New, SORT | Required by layout. `blnSpecial` checked only in the menu |
| `ajax.functions.php` | GET or POST `type` plus the fields for that type | Not checked |
| `test.php` | POST Filter and a client-side reload | Not checked |

AJAX `type` values: `ajaxSearchShipToAccountXML`, `ajaxLoadRegionBillToAccountsXML`, `ajaxLoadRegionShipToAccountsXML`, `ajaxReloadProductsXML`. The response is `text/xml` unless `header` was posted. `&` is replaced with `&amp;` and `%C9` with `%C3%89` on the whole document. A failed query returns `<data><error>query</error></data>`. Missing request keys return an empty string (`ajaxRequest`).

`REFERENCE_INCLUDE` stops the file from echoing XML when set. No other file in the repo sets it.

## Front end

The requisition page script is inline in `requisitionForm.php`, not in `scripts.js`. It uses jQuery AJAX (`$.ajax`) against `ajax.functions.php`. `jsParseRows` returns null unless the document has `getElementsByTagName` and a `rows` element. Callers then alert and return before removing options.

`custom_urldecode` rewrites selected percent-encodings, then calls `decodeURIComponent`.

`uniqueID` starts at `-1` and decreases for each added line so `Save` treats the row as an insert.

`scripts.js` supplies `d`, `formatNumber`, and `urldecode`. The rest of that file (course, client, candidate, contact popups) is not referenced by the freestock pages that were read. `md5.js` is in the javascript folder and is not included by the header.

Styles: `include/style/style.css`, `menu.css`, `CalendarControl.css`. A second calendar stylesheet `CalendarContro2l.css` (the filename contains an extra `2` placement) is not linked by the header. Images: `include/images/snell_logo.jpg` (logo and favicon). `snell_logo.png` and `rgbc_logo.jpg` are present. The RGBC image is only mentioned in a comment in the login layout.

## Background work, storage, monitoring

- Background work inside the app is the `sysCronJob` insert. The consumer is external. **Unknown**.
- File storage for uploads was not found.
- PHP sessions are files. `sess_*` files under `Sessions/` are tracked in Git (the clone has 13). `.gitignore` lists `Sessions/`, which does not untrack files that were committed earlier. `.gitignore` also lists `snell_freestock_backup_*/`. Those backup directories are not in the clone.
- No application log framework, metrics exporter, or health check was found. `mstUsageLog` is the only audit insert.
- `test.php` is the only diagnostic page, and it is unlinked.

## Security observations

These describe the code. They are not a penetration test.

- Database credentials are hardcoded in `include/system.inc.php`.
- List filters, login, user save, and several AJAX statements concatenate SQL. `PostedRequisitionID` and the 2026-09-18 item-key checks are narrower than the rest of the SQL.
- `RemoveItem` deletes by item id only.
- `ajax.functions.php` and `test.php` have no session gate.
- `user.php` has no `blnSpecial` gate of its own.
- Session files in the Git tree can hold serialized session payloads. They should be reviewed by a person before any further copy is made. This document does not quote them.
- `get_magic_quotes_gpc` remains in `odbc::qs` and `gpc_extract`. On PHP 8 that function is undefined. `gpc_extract` calls it via `ini_get`, which returns false. `odbc::qs` calls it as a function when that method runs.

## Development and testing

- No PHPUnit, no fixture, no CI config, no Sonar project file in Git. The onboarding scan named `sonar-project.properties` and `sonarqube_scan.bat` at the root. `git log --all` for those paths is empty.
- `test.php` is a manual page.
- Local development implied by the code: host name `sql-server` or `localhost`, session directory `snell_freestock_PHP\Sessions`, and on `sql-server` the fixed Windows user `silicon\pj`. How to obtain the ODBC DSN is not documented.
- Git identity of the work is in the project plan. Pull requests recorded in merge commits: #1 `FRS_debug_fixes` (2026-09-17), #2 `sync/from-live-20260918` (2026-09-18).

## Related code

`snell-freestock-5.6` is a separate Bitbucket repository with its own Silicon Ark folder. It shares this screen title and a similar file layout. Its `main` (`812337f`, 2025-06-13) is not this `master`. Compare the two before copying a defect or a requirement from one write-up to the other. On this revision the Azure AD username override is commented out; that was called out as active code in the 5.6 notes.
