toga-ai 1.0.526 → 1.0.528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/features/error-capture-1-0.md +17 -1
- package/knowledge/1.0/apps/togadesk/features/ticket-lifecycle.md +15 -1
- package/knowledge/1.0/apps/togaview/INDEX.md +3 -2
- package/knowledge/1.0/apps/togaview/architecture.md +24 -3
- package/knowledge/1.0/apps/togaview/features/login-flows.md +54 -1
- package/knowledge/1.0/apps/togaview/features/ticket-detail-page.md +116 -8
- package/knowledge/1.0/apps/togaview/workflows/git-and-php-tooling-under-the-underscore-app-root.md +58 -0
- package/knowledge/1.0/standards/backend-php.md +6 -3
- package/knowledge/2.0/apps/_underscore/INDEX.md +3 -1
- package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +20 -2
- package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +25 -4
- package/knowledge/2.0/apps/_underscore/features/string-html-entity-helpers.md +82 -0
- package/knowledge/2.0/apps/_underscore/features/togaiq-gateway-client.md +155 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +100 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/compass-canada/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/features/french-order-email-localization.md +130 -2
- package/knowledge/clients/compass-usa/features/approval-decision-flow.md +20 -1
- package/knowledge/clients/compass-usa/features/mr-ma-order-approval-and-status.md +16 -2
- package/knowledge/clients/rumcsi/profile.md +12 -1
- package/knowledge/clients/tow-foundation/profile.md +31 -3
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-04
|
|
10
|
-
owners: ["jcardinal"]
|
|
10
|
+
owners: ["jcardinal", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/error/capture.php
|
|
13
13
|
- library/app/error.php
|
|
@@ -200,6 +200,18 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
|
|
|
200
200
|
the class+entryPoint fallback changes hashes, so open issues re-fingerprint on first occurrence
|
|
201
201
|
after deploy. Deliberate; merge old hashes onto the curated Issue with the Tools
|
|
202
202
|
merge-fingerprint box.
|
|
203
|
+
- **⚠ The legacy pre-Sentry handler dumps the entire config on any error it renders.**
|
|
204
|
+
`library/app/DELETE_DOA_PRE-SENTRY_error.php` is a dead-named but still-reachable handler whose
|
|
205
|
+
`htmlBacktrace()` does `print_r($entry['args'])` for **every** frame. On 2026-08-05 a real
|
|
206
|
+
error page on **https://towfoundation.togaview.com** rendered ~26 pages containing the whole
|
|
207
|
+
config array — production DB host/user/password, SES SMTP credentials, NetSuite credentials,
|
|
208
|
+
Payeezy API key/secret/merchant token, SSL Store token — plus ticket PII. The current
|
|
209
|
+
`library/app/error.php` is safe (it dumps only under `App_Registry::inDevMode()`), but **both**
|
|
210
|
+
handlers gate on `inDevMode()`, so the affected host was **evaluating as dev mode in
|
|
211
|
+
production**. Two actions: (1) verify `inDevMode()` per host in production before trusting any
|
|
212
|
+
error page not to leak, and (2) delete the legacy handler. Rotation of every exposed credential
|
|
213
|
+
**was escalated to Jeff on 2026-08-05; confirmation that it was completed is still owed** —
|
|
214
|
+
treat those credentials as compromised until confirmed. (No values are recorded here or anywhere in the KB.)
|
|
203
215
|
- **Pre-existing committed secrets (location only — not fixed, out of scope).**
|
|
204
216
|
`library/app/cloud.php` holds a hardcoded **AWS access key id and secret access key** as
|
|
205
217
|
class constants/statics (~lines 23–24), used by `App_Cloud::$preCommands` for every 1.0 AWS CLI
|
|
@@ -208,6 +220,10 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
|
|
|
208
220
|
|
|
209
221
|
## Change history
|
|
210
222
|
|
|
223
|
+
- 2026-08-05 — Recorded the legacy `DELETE_DOA_PRE-SENTRY_error.php` handler leaking the full
|
|
224
|
+
config array (prod DB / SES / NetSuite / Payeezy / SSL Store credentials + ticket PII) on a
|
|
225
|
+
live towfoundation.togaview.com error page, meaning that host evaluates as dev mode in
|
|
226
|
+
production; rotation escalated to Jeff, confirmation still owed. (mhammontree)
|
|
211
227
|
- 2026-08-04 (latest, **uncommitted/undeployed** at time of writing) — **Mirrored 2.0's reversal:
|
|
212
228
|
`$GLOBALS` is now captured** into a top-level `GLOBALS` group in `capture.php`
|
|
213
229
|
(`buildGlobalsContext()`, `sanitizeForContext()`, `CONTEXT_GLOBALS_TO_EXCLUDE`,
|
|
@@ -6,7 +6,7 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-05
|
|
10
10
|
owners: ["mhammontree", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/classes/class.ticket.php
|
|
@@ -109,6 +109,17 @@ backfill (e.g. craftex:
|
|
|
109
109
|
`UPDATE TOGaDeskSupport.tickets SET customerid = 932297 WHERE clientid = 169 AND customerid IS NULL;`).
|
|
110
110
|
|
|
111
111
|
## Gotchas / known issues
|
|
112
|
+
- **`tickets.escalateToStartech` has NO migration anywhere — schema drift.** The column is
|
|
113
|
+
referenced by `App_Model_TogaDesk_Ticket` (`FIELDTYPE_INT`), `quickactions.php` (sets `1`) and
|
|
114
|
+
`manage.php` (checks `=== null`), and **exists in production**, but the `dbchanges` repo has no
|
|
115
|
+
migration for it; the only `escalateToStartech` SQL in any repo is the 2.0 sibling
|
|
116
|
+
`dbchanges2/Client_Pcmaticb2b/2026-06-22-pcmaticb2b-enhancements.sql`
|
|
117
|
+
(`c_escalateToStartech TINYINT(1) UNSIGNED NULL DEFAULT NULL`). A local dev DB missing the
|
|
118
|
+
column fails **every** ticket INSERT with MySQL error **1054**. Apply it locally; no migration
|
|
119
|
+
file is needed (prod already has it).
|
|
120
|
+
**Diagnosis technique worth reusing:** diff the model's `public $field` list against
|
|
121
|
+
`information_schema.COLUMNS` to find *every* gap at once — MySQL reports only the first unknown
|
|
122
|
+
column, so fixing them one error at a time is slow and misleading.
|
|
112
123
|
- `'Resolved'` is STILL missing from the `addReply()` switch — a client reply to a Resolved
|
|
113
124
|
ticket changes nothing. Known gap, awaiting product decision.
|
|
114
125
|
- **There is NO 72-hour reopen window in code**, despite SME belief. `Closed` → `Reopened` is
|
|
@@ -135,6 +146,9 @@ backfill (e.g. craftex:
|
|
|
135
146
|
via `emailToTicket()`.
|
|
136
147
|
|
|
137
148
|
## Change history
|
|
149
|
+
- 2026-08-05 — recorded `tickets.escalateToStartech` schema drift (in prod, no `dbchanges`
|
|
150
|
+
migration; missing column fails every ticket INSERT with error 1054 locally) and the
|
|
151
|
+
model-fields-vs-`information_schema` diffing technique, from TRUE-80671 (mhammontree)
|
|
138
152
|
- 2026-07-09 — mapped the four reply entry points (togaview portal bypasses `addReply()` so
|
|
139
153
|
never transitions status); documented `emailToTicket()` adminid-overwrite reply
|
|
140
154
|
misclassification, the `origin`-absence agent-reply signal, and togadesk↔togaview shared-DB
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa View Architecture](architecture.md) | TOGa View is the client-facing support portal, served per client at `<client>.togaview.com`. | index.php, reset.php, _/app/framework.php, _/app/frameworkindex.php, _/browser/pagination.php, mvc/login/post.php, mvc/ticket/get.php |
|
|
6
|
-
| [TOGa View Login Flows & Session Variables](features/login-flows.md) | `mvc/login/post.php` tries login flows in order; the first match wins. | mvc/login/post.php, _/app/framework.php |
|
|
6
|
+
| [TOGa View Login Flows & Session Variables](features/login-flows.md) | `mvc/login/post.php` tries login flows in order; the first match wins. | mvc/login/post.php, _/app/framework.php, _/app/frameworkindex.php, mvc/msp_client_dashboard/post.php |
|
|
7
7
|
| [MSP Dashboard & Ticket Visibility Rules](features/msp-dashboard.md) | Why tickets "disappear" in the TOGa View client portal: different pages scope tickets **differently**, and the MSP pages depend on `tickets.customerid` and `SMB | common/togaview/msp_dashboard.php, mvc/msp_client_dashboard, mvc/enterprise_dashboard, mvc/support/support.php |
|
|
8
8
|
| [Retail Commerce Lifecycle (winback, purchase, claims, activation)](features/retail-commerce-lifecycle.md) | Beyond support, TOGa View hosts the **retail tech-support commerce flows**: subscription purchase/renewal (with payment), service activation, and insurance-styl | mvc/winback_landing/get.php, mvc/winback_payment/get.php, mvc/winback_payment/post.php, mvc/purchase/get.php, mvc/service_activation/get.php, mvc/claims/get.php, mvc/techsupport_app/get.php |
|
|
9
|
-
| [Ticket Detail Page Security (common
|
|
9
|
+
| [Ticket Detail Page Security & Its Three Forks (common/*/ticket.php)](features/ticket-detail-page.md) | `common/togaview/ticket.php` is the ticket detail page for nearly ALL hosts — only towfoundation/newcenturyholdingsllc have their own variants; every other clie | common/togaview/ticket.php, common/towfoundation/ticket.php, common/newcenturyholdingsllc/ticket.php, mvc/ticket/get.php, mvc/ticket/post.php |
|
|
10
|
+
| [Git & php -l Tooling Against the `_/` App Root (MSYS path mangling)](workflows/git-and-php-tooling-under-the-underscore-app-root.md) | In **Git Bash on Windows**, any command argument beginning with `_/` is rewritten by MSYS path conversion before the tool sees it. | _/app/framework.php, _/app/frameworkindex.php |
|
|
10
11
|
| [Route 53 Hosted Zone Migration Between AWS Accounts](workflows/route53-cross-account-zone-migration.md) | How to move a domain's DNS records (a Route 53 hosted zone) from one AWS account to another. | |
|
|
@@ -6,7 +6,7 @@ project: TOGa View
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-05
|
|
10
10
|
owners: ["mhammontree", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- index.php
|
|
@@ -46,7 +46,9 @@ generic `common/togaview/*.php` templates, while a handful have full per-client
|
|
|
46
46
|
superglobals, starts the session (`HttpOnly`/`Secure`), inits Sentry, and registers the
|
|
47
47
|
NetSuite toolkit + SAML.
|
|
48
48
|
- **`frameworkindex.php`** is the page-shell renderer (CSS/JS registration, page title).
|
|
49
|
-
|
|
49
|
+
A near-duplicate `framework_DOA.php` was **deleted 2026-08-05** (440 dead lines carrying the
|
|
50
|
+
same unguarded name-split bug as the live file); if one reappears, delete it rather than
|
|
51
|
+
maintaining it.
|
|
50
52
|
|
|
51
53
|
## Page routing (stylePath model)
|
|
52
54
|
`index.php → App_Framework_TogaView::renderIndex()` (in `_/app/framework.php`):
|
|
@@ -67,6 +69,17 @@ generic `common/togaview/*.php` templates, while a handful have full per-client
|
|
|
67
69
|
**Implication:** a change to `common/togaview/<page>.php` affects nearly every client at
|
|
68
70
|
once; a per-client behavior change belongs in a host/stylePath variant or an inline branch.
|
|
69
71
|
|
|
72
|
+
**Fork parity is a security requirement.** `common/{togaview,towfoundation,newcenturyholdingsllc}/ticket.php`
|
|
73
|
+
are three near-duplicates; TRUE-78835's ownership checks and int-cast landed in only one, leaving
|
|
74
|
+
two hosts with a live cross-tenant IDOR for two months (found and fixed in TRUE-80671). Any fix to
|
|
75
|
+
a forked page must be applied to every fork, and any review of one must check the others.
|
|
76
|
+
|
|
77
|
+
**Open question — is `newcenturyholdingsllc` a real tenant?** It exists only as a togaview host
|
|
78
|
+
with its own `common/newcenturyholdingsllc/` page fork and a SAML branch that auto-creates
|
|
79
|
+
`people` rows, but it has **no client profile** in the knowledge base and its tenant status is
|
|
80
|
+
unconfirmed. The TRUE-80671 fixes shipped to its fork and are recorded on the shared togaview
|
|
81
|
+
feature docs. Deliberately not onboarded — treat as an open question, not an oversight.
|
|
82
|
+
|
|
70
83
|
## MVC request lifecycle
|
|
71
84
|
- `App_MVC::parseRoute()` (in `library`) turns the request URI into route parts and resolves
|
|
72
85
|
`mvc/<route…>/<method>.php`, where `<method>` is `get` or `post` (from the HTTP verb,
|
|
@@ -95,7 +108,9 @@ divergent shapes (read `emailAddress` with `email` fallback).
|
|
|
95
108
|
|
|
96
109
|
**SAML** (rumcsi, towfoundation, newcenturyholdingsllc) is handled in `framework.php`
|
|
97
110
|
(`meta`/`acs`/`sls`); newcenturyholdingsllc and towfoundation **auto-create `people` rows** for
|
|
98
|
-
unknown SAML users. rumcsi additionally accepts a legacy `?hash=<base64 email>` auto-login
|
|
111
|
+
unknown SAML users. rumcsi additionally accepts a legacy `?hash=<base64 email>` auto-login — **an unauthenticated
|
|
112
|
+
account-takeover path that appears to be the client's actual production login**; do not remove it
|
|
113
|
+
before completing the `acs` handler (see [login flows](features/login-flows.md)).
|
|
99
114
|
|
|
100
115
|
## Dashboards
|
|
101
116
|
Five dashboards, one per audience/business model, each delegating to a client override or the
|
|
@@ -154,6 +169,12 @@ retail-commerce-lifecycle feature doc.
|
|
|
154
169
|
- **Hardcoded Braintree credentials (including a private key) are committed under
|
|
155
170
|
`mvc/winback_payment/`** — these should be rotated and moved to `config.<env>.ini`
|
|
156
171
|
secret management. Flagged 2026-06-15; not yet remediated.
|
|
172
|
+
- **Production can render dev-mode error pages.** A live towfoundation error page dumped the
|
|
173
|
+
entire config array (prod DB, SES, NetSuite, Payeezy, SSL Store credentials) because both error
|
|
174
|
+
handlers gate on `App_Registry::inDevMode()` and that host evaluated as dev in production.
|
|
175
|
+
Verify `inDevMode()` per host. Rotation of every exposed credential was escalated to Jeff on
|
|
176
|
+
2026-08-05; **confirmation that it was completed is still owed** — treat those credentials as
|
|
177
|
+
compromised until confirmed.
|
|
157
178
|
- `ENVIRONMENT` defaults to `worker` when unset → wrong config file.
|
|
158
179
|
|
|
159
180
|
## Related docs
|
|
@@ -6,11 +6,13 @@ project: TOGa View
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-05
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- mvc/login/post.php
|
|
13
13
|
- _/app/framework.php
|
|
14
|
+
- _/app/frameworkindex.php
|
|
15
|
+
- mvc/msp_client_dashboard/post.php
|
|
14
16
|
related:
|
|
15
17
|
- 1.0/apps/togaview/features/msp-dashboard.md
|
|
16
18
|
- 1.0/apps/togaview/features/ticket-detail-page.md
|
|
@@ -51,13 +53,64 @@ end-user flows.
|
|
|
51
53
|
- staplesprotection: inline branch, end-user style, divergent session keys (see table).
|
|
52
54
|
- rumcsi / newcenturyholdingsllc: SAML handled in `_/app/framework.php`, not login/post.php.
|
|
53
55
|
|
|
56
|
+
## Session-name building — never index `[1]` unguarded
|
|
57
|
+
Several flows built `$_SESSION['clientName'] = $user_name[0] . ' ' . $user_name[1]` from
|
|
58
|
+
`explode(' ', $name)`. A **mononym or shared mailbox** (`operations@`, `kindred@`) — or a name
|
|
59
|
+
stored with a leading space — makes `[1]` undefined, and `App_Error::handleError()` promotes
|
|
60
|
+
every notice to an `ErrorException` and `exit()`s. The result was **total lockout**: those users
|
|
61
|
+
simply could not log in, with no page error anyone would report — a silent outage class. Fixed at
|
|
62
|
+
4 sites (`mvc/login/post.php` towfoundation/newcenturyholdingsllc/rumcsi blocks, plus the rumcsi
|
|
63
|
+
bootstrap in `_/app/framework.php`) to `trim($user_name[0] . ' ' . ($user_name[1] ?? ''))`.
|
|
64
|
+
|
|
65
|
+
## `sqlProtect(sqlEscape())` removed from the login path
|
|
66
|
+
`mvc/login/post.php`, `mvc/msp_client_dashboard/post.php`, `_/app/framework.php` and
|
|
67
|
+
`_/app/frameworkindex.php` wrapped already-escaped emails in `App_Database::sqlProtect()`, which
|
|
68
|
+
**strips** `% \ / * " ' ` and ` or` — so `o'brien@x.com` became `obrien@x.com` and the login
|
|
69
|
+
lookup silently found nothing. Removed, and `email LIKE '…'` changed to `=` in the same change
|
|
70
|
+
(see the coupling note in the
|
|
71
|
+
[ticket detail page doc](ticket-detail-page.md#no-sqlprotect-on-escaped-values-email-lookups-use--not-like)).
|
|
72
|
+
|
|
54
73
|
## Gotchas / known issues
|
|
74
|
+
|
|
75
|
+
### OPEN SECURITY ITEM — rumcsi `?hash=` unauthenticated account takeover (deliberately left in place)
|
|
76
|
+
`_/app/framework.php` authenticates a rumcsi user on nothing but
|
|
77
|
+
`base64_decode(urldecode($_GET['hash']))` matching an active `people` row for the rumcsi
|
|
78
|
+
clientid — **no password, signature, nonce, or expiry** — then populates ~18 session keys and
|
|
79
|
+
redirects to `/enterprise_dashboard`. Anyone who can guess a RUMC user's email can log in as
|
|
80
|
+
them.
|
|
81
|
+
|
|
82
|
+
It was removed during TRUE-80671 and then **deliberately reverted**, because it may BE the
|
|
83
|
+
production login mechanism: the rumcsi SAML `acs` handler does `App_MVC::routeTo('/')` on success
|
|
84
|
+
and **never populates `$_SESSION`** (unchanged since commit 9e7d306, "phase one … make sure
|
|
85
|
+
redirect works"); repo-wide only two places ever build a rumcsi session — this hash block and
|
|
86
|
+
`mvc/login/post.php:462`, and the latter is unreachable because the SAML gate redirects to the
|
|
87
|
+
IdP before a password POST can be processed; and RUMC login is confirmed **working in
|
|
88
|
+
production**. Nothing in togaview/togadesk/library generates such a link, so links are presumably
|
|
89
|
+
generated outside our codebase.
|
|
90
|
+
|
|
91
|
+
- **Next step:** obtain the post-SSO redirect URL from Jonathan to confirm whether it carries
|
|
92
|
+
`hash=`.
|
|
93
|
+
- **Proper fix:** complete the rumcsi `acs` handler (mirror newcenturyholdingsllc:
|
|
94
|
+
`$auth->getNameId()` → `people` lookup → session build) — **not** restoring the bypass.
|
|
95
|
+
- Note the surviving SAML branch is gated on `!inTestMode()`, so **test mode leaves rumcsi with
|
|
96
|
+
no login path at all**.
|
|
97
|
+
|
|
98
|
+
### Other
|
|
99
|
+
- **`people.password` is a bare SHA-1** (`mvc/login/post.php:21/96/471`) and the hash is also
|
|
100
|
+
carried in `$_SESSION['Contact']['password']`. Known and accepted for now — togaview is
|
|
101
|
+
migrating to 2.0, so no remediation is planned here; do not "fix" it as a side effect of
|
|
102
|
+
another ticket.
|
|
55
103
|
- Code consuming the session must read `emailAddress` with an `email` fallback (and tolerate
|
|
56
104
|
missing `togadeskClientId`) or it rejects all staplesprotection users — this broke the
|
|
57
105
|
ticket.php ownership check until the fallback was added (June 2026).
|
|
58
106
|
- The flows are order-dependent: a contact matching an earlier flow never reaches later ones.
|
|
59
107
|
|
|
60
108
|
## Change history
|
|
109
|
+
- 2026-08-05 (TRUE-80671, uncommitted) — Fixed the unguarded `$user_name[1]` session-name build
|
|
110
|
+
that **locked mononym / shared-mailbox users out entirely** (4 sites); removed
|
|
111
|
+
`sqlProtect(sqlEscape())` and switched email lookups to `=`; recorded the rumcsi `?hash=`
|
|
112
|
+
auto-login as an open takeover risk that was intentionally left in place pending confirmation
|
|
113
|
+
of the real SSO redirect. (mhammontree)
|
|
61
114
|
- 2026-06-12 — documented from craftex MSP portal debugging session (mhammontree)
|
|
62
115
|
|
|
63
116
|
## Related docs
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Ticket Detail Page Security (common
|
|
2
|
+
title: Ticket Detail Page Security & Its Three Forks (common/*/ticket.php)
|
|
3
3
|
framework: "1.0"
|
|
4
4
|
repo: togaview
|
|
5
5
|
project: TOGa View
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-05
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- common/togaview/ticket.php
|
|
13
|
+
- common/towfoundation/ticket.php
|
|
14
|
+
- common/newcenturyholdingsllc/ticket.php
|
|
15
|
+
- mvc/ticket/get.php
|
|
16
|
+
- mvc/ticket/post.php
|
|
13
17
|
related:
|
|
14
18
|
- 1.0/apps/togaview/features/login-flows.md
|
|
15
19
|
- 1.0/apps/togaview/features/msp-dashboard.md
|
|
@@ -21,6 +25,24 @@ towfoundation/newcenturyholdingsllc have their own variants; every other client'
|
|
|
21
25
|
back to it. Hardened June 2026 against IDOR (ticket view + file attachment download) and a
|
|
22
26
|
"Deactivated Seat" false positive that blocked MSP admins.
|
|
23
27
|
|
|
28
|
+
⚠ **THREE near-duplicate forks exist, and a fix applied to one is NOT applied to the others.**
|
|
29
|
+
`mvc/ticket/get.php` selects: host in
|
|
30
|
+
`App_Framework_TogaView::HOSTS_USING_TOGA_TECH_STYLE` (= `['towfoundation',
|
|
31
|
+
'newcenturyholdingsllc']`) → `common/<host>/ticket.php`; otherwise `common/togaview/ticket.php`.
|
|
32
|
+
**Every security or bug fix here must be applied to all three forks.** TRUE-80671 found
|
|
33
|
+
**three separate hardenings** (ticket-view ownership, attachment-download ownership, the
|
|
34
|
+
`ticketId` int-cast) that had only ever landed in `common/togaview/ticket.php` — commit
|
|
35
|
+
9052672/8cfc80e of TRUE-78835 patched one of three files, leaving towfoundation and
|
|
36
|
+
newcenturyholdingsllc fully exploitable for two months. Treat fork parity as a checklist item,
|
|
37
|
+
not a nicety.
|
|
38
|
+
|
|
39
|
+
**Open question — `newcenturyholdingsllc` has a fork but no client profile.** It exists only as a
|
|
40
|
+
togaview host with its own `common/newcenturyholdingsllc/` pages and a SAML branch that
|
|
41
|
+
auto-creates `people` rows; whether it is a real tenant is **unconfirmed**, so it was deliberately
|
|
42
|
+
not onboarded as a client (2026-08-05). Its TRUE-80671 fixes are recorded here on the shared docs.
|
|
43
|
+
If you learn its formal name and TOGa Desk `clients.id`, onboard it — this is an open question,
|
|
44
|
+
not an oversight.
|
|
45
|
+
|
|
24
46
|
## How it works
|
|
25
47
|
|
|
26
48
|
### Ownership check (IDOR fix)
|
|
@@ -44,8 +66,52 @@ requester email isn't a provisioned SMB seat user (the SMBContractItems JOIN ret
|
|
|
44
66
|
which is NORMAL for email-intake tickets, so it must never apply to MSP/CUSTOMER viewers.
|
|
45
67
|
|
|
46
68
|
### Input handling
|
|
47
|
-
`$_GET['ticketId']` is int-cast once at the top — it is interpolated into
|
|
48
|
-
|
|
69
|
+
`$_GET['ticketId']` is int-cast once at the top of **each** fork — it is interpolated into
|
|
70
|
+
several queries (was a live SQL injection in `common/togaview/` before June 2026 and in
|
|
71
|
+
`common/towfoundation/` until 2026-08-05).
|
|
72
|
+
|
|
73
|
+
**Why an uncast id is genuinely injectable:** `App_Model` interpolates the primary key **raw**
|
|
74
|
+
into its SELECT (`library/app/model.php:218` and `:294`; the `(int)` at `:404` covers only the
|
|
75
|
+
save path). So `new App_Model_TogaDesk_Ticket($_GET['ticketId'])` or
|
|
76
|
+
`new App_Model_TogaDesk_File($_GET['fileId'])` with an uncast value is an injection point —
|
|
77
|
+
constructing a model is not a safe boundary.
|
|
78
|
+
|
|
79
|
+
### Requester-name splitting (must be guarded)
|
|
80
|
+
Each fork renders the requester's first/last name via `explode(' ', $name)`. A **mononym or
|
|
81
|
+
shared mailbox** (`operations@`, `kindred@`) — or a name stored with a **leading space** —
|
|
82
|
+
yields a 1-element array, and reading `[1]` raises `Undefined offset: 1`. Because
|
|
83
|
+
`App_Error::handleError()` promotes **every** notice to an `ErrorException` and `exit()`s, that
|
|
84
|
+
notice is a hard 500, not cosmetic. Guard with
|
|
85
|
+
`if (!empty($ticketUserInformation) && isset($ticketUserInformation['name']))` and use
|
|
86
|
+
`$parts[1] ?? ''`.
|
|
87
|
+
|
|
88
|
+
⚠ **Do not assign firstName/lastName unconditionally to "simplify" the guard.** Doing so makes
|
|
89
|
+
an empty no-match result **non-empty**, which silently bypasses the `empty($ticketUserInformation)`
|
|
90
|
+
deactivated-seat fallback further down the same file. `App_Database::fetchRow()` returns `[]`
|
|
91
|
+
(not `false`) on no match, so emptiness *is* the no-match signal.
|
|
92
|
+
|
|
93
|
+
### Custom fields are keyed off the ticket's own client
|
|
94
|
+
The towfoundation fork was a copy-paste of the rumcsi branch and queried
|
|
95
|
+
`tickets_customfields WHERE clientid = (SELECT id FROM clients WHERE name = "Richmond
|
|
96
|
+
University Medical Center")` — serving TOW Foundation **RUMC's** field definitions
|
|
97
|
+
(cross-tenant leak). It now uses `(int) $ticketDetails->clientid`
|
|
98
|
+
(`App_Model_TogaDesk_Ticket::$clientid` is `FIELDTYPE_INT`) with no hardcoded client name.
|
|
99
|
+
Never resolve a tenant by literal client *name* in these files.
|
|
100
|
+
|
|
101
|
+
### No `sqlProtect()` on escaped values; email lookups use `=`, not `LIKE`
|
|
102
|
+
`App_Database::sqlProtect(App_Database::sqlEscape($x))` was removed at **39 sites across 7
|
|
103
|
+
files**. `sqlProtect()` is `str_ireplace(['%','\\','/','*',' or','"',"'"], '', $input)` — it
|
|
104
|
+
**deletes** those characters, stripping the backslashes `sqlEscape()` just added, so
|
|
105
|
+
`o'brien@x.com` silently becomes `obrien@x.com` and the lookup finds nothing. It also mangles
|
|
106
|
+
ordinary strings ("Victor Ortiz" → "Victt Ortiz", because ` or` is stripped case-insensitively).
|
|
107
|
+
It adds **no** injection protection — the escape already made the value safe.
|
|
108
|
+
|
|
109
|
+
**The `LIKE` → `=` change must ship with the removal.** `sqlProtect` stripping `%` was the only
|
|
110
|
+
thing preventing a `%` in an address from acting as a LIKE wildcard; removing it while keeping
|
|
111
|
+
`LIKE` would introduce over-matching. Verified behaviorally identical against the real schema:
|
|
112
|
+
`people.email` / `contacts.email` are `utf8mb3_general_ci`, `Customers.email` and per-client
|
|
113
|
+
`Contacts.emailAddress` are `latin1_swedish_ci` — all case-insensitive VARCHARs, `people.email`
|
|
114
|
+
is BTREE-indexed, and no site relied on wildcards.
|
|
49
115
|
|
|
50
116
|
## Data model
|
|
51
117
|
`TOGaDeskSupport.tickets`, `tickets_replies`, `files`; TOGA-side `SMBContractItems` →
|
|
@@ -58,14 +124,56 @@ this file (was a live SQL injection before June 2026).
|
|
|
58
124
|
`emailAddress`/`togadeskClientId`) — the ownership check's email fallback exists for them.
|
|
59
125
|
|
|
60
126
|
## Gotchas / known issues
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
|
|
128
|
+
### OPEN — the ownership check is defeated for retail by a shared client id
|
|
129
|
+
Both RetailServices logins hardcode `$_SESSION['togadeskClientId'] = 11`
|
|
130
|
+
(`mvc/login/post.php:536` and `:560`) and **every** retail ticket is created with that value
|
|
131
|
+
(`mvc/ticket/post.php:220,285`). So the ownership check's client-match arm evaluates `11 == 11`
|
|
132
|
+
for every retail session and **never reaches the email arm**: any retail customer can read any
|
|
133
|
+
other retail customer's ticket and attachments by iterating `ticketId`/`fileId`. Retail routes
|
|
134
|
+
to `common/togaview/ticket.php` (there is no `common/walmart*/ticket.php`).
|
|
135
|
+
**Suggested fix:** treat id 11 as a shared bucket — require the email arm when
|
|
136
|
+
`togadeskClientId == 11` — and replace the magic `11` with a named constant. Not fixed in
|
|
137
|
+
TRUE-80671 by explicit developer decision (one login issue at a time; togaview is migrating to
|
|
138
|
+
2.0).
|
|
139
|
+
|
|
140
|
+
### OPEN — portal-uploaded attachments can never be downloaded
|
|
141
|
+
Upload writes to **S3** (`App_Cloud::copyFileToS3($tempPath, 'asifiles',
|
|
142
|
+
'/togadesk-<env>/uploads/<name>')`, `mvc/ticket/post.php:160`; `files.file` stores only the bare
|
|
143
|
+
filename), but the download handler reads the **local filesystem** at
|
|
144
|
+
`$_SERVER['DOCUMENT_ROOT'] . $config['internal']['kb_documents_path'] . '/' . $file->file` — the
|
|
145
|
+
**knowledge-base** document path, evidently copy-pasted from a KB handler and never adapted for
|
|
146
|
+
S3. Fails **silently**: `file_exists()` fails → `routeTo(...&invalidFile=true)` → "Invalid File"
|
|
147
|
+
toaster, not an error, which is why it went unnoticed. Same in all three forks.
|
|
148
|
+
`App_Cloud::getFileFromS3()` (`library/app/cloud.php:540`) exists and is unused here. Verify
|
|
149
|
+
against production (prod `DOCUMENT_ROOT/uploads` may be S3-synced) before sizing.
|
|
150
|
+
|
|
151
|
+
### Per-developer config must carry the `kb_*` keys
|
|
152
|
+
Attachment download reads `$config['internal']['kb_documents_path']`. A dev config missing the
|
|
153
|
+
five `kb_*` keys (`kb_documents_path`, `kb_articles_path`, `kb_faqs_path`, `kb_tasks_path`,
|
|
154
|
+
`kb_general_path`) — which prod/alpha/beta/stage and most dev configs carry — raises
|
|
155
|
+
"Undefined index", **fatal** under `App_Error`. The active config is chosen by
|
|
156
|
+
`getenv('ENVIRONMENT')`, set via Apache `SetEnv ENVIRONMENT` in `httpd.conf`. Copy the `kb_*`
|
|
157
|
+
block when creating a new `config.dev-*.ini`.
|
|
158
|
+
|
|
159
|
+
### Other
|
|
160
|
+
- Pre-existing wart (untouched): unescaped `$emailAddress` in the walmart branch query.
|
|
63
161
|
- Pending as of 2026-06-12: MSP/CUSTOMER bypass for the ownership check (MSP session's
|
|
64
162
|
`togadeskClientId` is the MSP's own client id — matches their customers' tickets since those
|
|
65
|
-
carry the MSP's clientid, but verify when an MSP views a ticket whose clientid differs)
|
|
66
|
-
|
|
163
|
+
carry the MSP's clientid, but verify when an MSP views a ticket whose clientid differs).
|
|
164
|
+
- TRUE-80671 changes were **working-tree only** (branch `TRUE-80671`, nothing committed) at
|
|
165
|
+
session end.
|
|
67
166
|
|
|
68
167
|
## Change history
|
|
168
|
+
- 2026-08-05 (TRUE-80671, uncommitted) — **Ported both TRUE-78835 ownership checks (ticket view
|
|
169
|
+
+ attachment download) into the towfoundation and newcenturyholdingsllc forks**, which had
|
|
170
|
+
neither — a live cross-tenant IDOR on both hosts; verified both directions live. Guarded the
|
|
171
|
+
requester name-split in all three forks (`Undefined offset: 1` → hard 500 for mononyms/shared
|
|
172
|
+
mailboxes); fixed the towfoundation fork serving **RUMC's** custom-field definitions
|
|
173
|
+
(hardcoded client name → `(int) $ticketDetails->clientid`); added the missing `(int)` cast on
|
|
174
|
+
`$_GET['ticketId']` in the towfoundation fork; removed
|
|
175
|
+
`sqlProtect(sqlEscape())` and switched email lookups to `=`. Recorded the retail shared-clientId
|
|
176
|
+
IDOR and the S3-vs-local attachment-download bug as open items. (mhammontree)
|
|
69
177
|
- 2026-06-12 — file-attachment IDOR fix (ownership check now covers fileId branch);
|
|
70
178
|
email-key fallback for staplesprotection (mhammontree)
|
|
71
179
|
- 2026-06 — ownership check added (ticket view), deactivated-seat gate restricted to
|
package/knowledge/1.0/apps/togaview/workflows/git-and-php-tooling-under-the-underscore-app-root.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Git & php -l Tooling Against the `_/` App Root (MSYS path mangling)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: togaview
|
|
5
|
+
project: TOGa View
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-05
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- _/app/framework.php
|
|
13
|
+
- _/app/frameworkindex.php
|
|
14
|
+
related:
|
|
15
|
+
- 1.0/apps/togaview/architecture.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
In **Git Bash on Windows**, any command argument beginning with `_/` is rewritten by MSYS
|
|
20
|
+
path conversion before the tool sees it. So `git diff HEAD -- _/app/framework.php` returns an
|
|
21
|
+
**empty diff** on a file you definitely changed, and `php -l _/app/framework.php` misfires the
|
|
22
|
+
same way. **Quoting does not help** — MSYS converts quoted arguments too.
|
|
23
|
+
|
|
24
|
+
This is not an edge case in togaview: the app root **is** `_/` (`App_Framework` resolves
|
|
25
|
+
`__APPROOT__` by walking up to a folder named `_`), so every framework file in the repo hits it.
|
|
26
|
+
In TRUE-80671 it caused **two separate subagent reviews to report a changed file as unchanged**,
|
|
27
|
+
which nearly sent a security fix out unreviewed.
|
|
28
|
+
|
|
29
|
+
## How it works — the safe forms
|
|
30
|
+
|
|
31
|
+
Use any one of these (all verified):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
MSYS_NO_PATHCONV=1 git diff HEAD -- "_/app/framework.php" # disable conversion
|
|
35
|
+
git diff HEAD -- "./_/app/framework.php" # leading ./ defeats it
|
|
36
|
+
git diff HEAD -- "*app/framework.php" # glob pathspec
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The same applies to `php -l` and to anything else taking a `_/`-prefixed path.
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
1. **Any review instruction, script, or agent prompt that names a path under `_/` must use one
|
|
44
|
+
of the safe forms above.** Prefer `MSYS_NO_PATHCONV=1`, which fixes the whole command.
|
|
45
|
+
2. **An empty diff on a file you know you changed is this bug**, not evidence of no change.
|
|
46
|
+
Confirm with `git status --porcelain` or a bare `git diff HEAD` (no pathspec) before
|
|
47
|
+
concluding anything.
|
|
48
|
+
3. Reviewing "no changes found" is a **failed review**, not a passing one — re-run it.
|
|
49
|
+
|
|
50
|
+
## Gotchas / known issues
|
|
51
|
+
- PowerShell and cmd are unaffected; the bug appears only under Git Bash/MSYS, so a teammate on
|
|
52
|
+
a different shell may not reproduce it.
|
|
53
|
+
- A bare `git diff HEAD` with **no** pathspec is always safe — path conversion only touches
|
|
54
|
+
arguments, and reviewing the whole diff avoids the trap entirely.
|
|
55
|
+
|
|
56
|
+
## Change history
|
|
57
|
+
- 2026-08-05 — Documented after two TRUE-80671 subagent reviews silently reported changed
|
|
58
|
+
`_/app/framework.php` as unchanged. (mhammontree)
|
|
@@ -5,7 +5,7 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-05
|
|
9
9
|
owners: [jcardinal, rgirish, mhammontree]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -192,7 +192,10 @@ In code you rarely write raw column types — an `App_Model` declares each colum
|
|
|
192
192
|
* Never interpolate raw user input into a query string. In order of preference:
|
|
193
193
|
1. **Use the `App_Model` layer** — it builds and escapes queries for you.
|
|
194
194
|
2. **When writing SQL by hand, escape every interpolated value with `App_Database::sqlEscape()`** before placing it in the query string. In `browser/` action files the helper `getVarEscaped('field')` wraps `sqlEscape()` for request values.
|
|
195
|
-
* `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively).
|
|
195
|
+
* `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively). **`sqlEscape()` alone is the injection control — nothing needs to be added to it.**
|
|
196
|
+
* **NEVER wrap an escaped value in `sqlProtect()`.** `App_Database::sqlProtect()` is `str_ireplace(['%','\\','/','*',' or','"',"'"], '', $input)` — it **deletes** those characters, including the backslashes `sqlEscape()` just added. `sqlProtect(sqlEscape($email))` turns `o'brien@x.com` into `obrien@x.com`, so the lookup silently finds nothing; it also mangles ordinary strings ("Victor Ortiz" → "Victt Ortiz", since ` or` is stripped case-insensitively). It is **data corruption, not defense in depth**. 39 such sites across 7 togaview files were removed in TRUE-80671.
|
|
197
|
+
* **If you remove `sqlProtect()` from a `LIKE` comparison, change `LIKE` to `=` in the same commit.** `sqlProtect` stripping `%` was the only thing stopping a `%` in user data from acting as a wildcard; removing it while keeping `LIKE` introduces over-matching. (Safe for email columns, which are case-insensitive VARCHARs — verify collation before generalizing.)
|
|
198
|
+
* `sqlProtect()`'s only legitimate use is sanitizing free-form **search** input, never a value you intend to match or store intact.
|
|
196
199
|
* Legacy uses **mysqli** (via `App_Database`), not PDO/prepared statements; escaping discipline at the call site is the control.
|
|
197
200
|
|
|
198
201
|
```php
|
|
@@ -347,7 +350,7 @@ class App_Model_Client extends App_Model {
|
|
|
347
350
|
* **Read results:** `fetchRow(&$res)`, `fetchOne(&$res, $rowIndex, $colIndexOrField)`, `numRows(&$res)`.
|
|
348
351
|
* **Writes:** `affectedRows($link)`, `getInsertId($link)`.
|
|
349
352
|
* **Cached lookups:** `lookupField($table, $aryLookup, $returnField, $link, $throwIfNotFound)`, `lookupRecord($table, $idValue, $link)`.
|
|
350
|
-
* **Escaping:** `sqlEscape()` (safe interpolation) and `sqlProtect()` (
|
|
353
|
+
* **Escaping:** `sqlEscape()` (the injection control — safe interpolation) and `sqlProtect()` (**lossy character-stripping filter; never wrap an escaped value in it**) — see SQL Injection Prevention.
|
|
351
354
|
|
|
352
355
|
## browser/ — Server-Rendered UI
|
|
353
356
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
|
|
16
16
|
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
|
|
17
17
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
18
|
-
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, worker2/Worker/Infrastructure/Email/Send.php |
|
|
18
|
+
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php |
|
|
19
19
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
20
20
|
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
21
21
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
30
30
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
31
31
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
|
|
32
|
+
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
32
33
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
33
34
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
35
|
+
| [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
|
|
34
36
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
35
37
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
36
38
|
| [USPS DPV Deliverability Verdict (is this address actually insurable/shippable?)](features/usps-dpv-deliverability.md) | **USPS returning HTTP 200 with a populated address is NOT evidence that the address is deliverable.** The authoritative signal is USPS's **DPV (Delivery Point V | _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Model/Client/Address.php, _underscore/Model/Rate/Entitlement.php |
|
|
@@ -14,6 +14,7 @@ related:
|
|
|
14
14
|
- ../../worker2/features/oneuptime-worker2-monitoring.md
|
|
15
15
|
- ../../worker2/features/creating-worker-actions.md
|
|
16
16
|
- ../../worker2/features/talos-transcript-ingestion.md
|
|
17
|
+
- togaiq-gateway-client.md
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
## Summary
|
|
@@ -49,11 +50,21 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
|
|
|
49
50
|
per attempt — that also gives you one api-log row per attempt.
|
|
50
51
|
- **Transport failures surface as a thrown Exception** from `execute()` (the equivalent of
|
|
51
52
|
`curl_error()`), not a return value. Catch it if your method has an error-return contract.
|
|
53
|
+
- **The outbound log row is COMMITTED immediately — use it as proof a call happened.** When
|
|
54
|
+
logging is on (the default), `execute()` writes a `_Model_Client_Logs_Api` row with
|
|
55
|
+
`direction = 'OUT'` and calls `transactionCommit(DB_CLIENT_LOGS)` (`ApiRequest.php` ~lines
|
|
56
|
+
253–270), so the row **survives even if the surrounding request later rolls back**. That makes
|
|
57
|
+
`SELECT … FROM Logs_<Client>.Api WHERE direction = 'OUT'` definitive evidence of whether an
|
|
58
|
+
outbound call was ever *attempted* — the first thing to check when an integration "returns
|
|
59
|
+
nothing" (no row = the code never ran / was never deployed; a row with a null `responseCode` =
|
|
60
|
+
the two-phase logging issue below).
|
|
52
61
|
- **Api logging depends on `DB_CLIENT_LOGS` being registered.** The logging branch writes via
|
|
53
62
|
`_Model_Client_Logs_Api`, whose `DATABASE` const is `_underscore::DB_CLIENT_LOGS`. In a context
|
|
54
63
|
that hasn't registered it (most workers, CLI harnesses) logging is a **silent no-op** — or
|
|
55
|
-
throws `Unknown database 'ClientLogs'`.
|
|
56
|
-
`
|
|
64
|
+
throws `Unknown database 'ClientLogs'`. In a bare CLI script that never called
|
|
65
|
+
`_Database::registerClientDatabases()`, that throw comes out of `execute()` itself and **looks
|
|
66
|
+
like a broken integration** when the API call is fine. Either register the logs DB under that
|
|
67
|
+
alias or pass `setLogging(false)` deliberately. See
|
|
57
68
|
[Creating Worker Actions](../../worker2/features/creating-worker-actions.md#gotchas).
|
|
58
69
|
- **OPEN / not fixed: logging happens in two halves around the HTTP call.** `execute()` inserts
|
|
59
70
|
the request row and **commits before** the call, then `save()`s the response fields **after**.
|
|
@@ -64,6 +75,13 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
|
|
|
64
75
|
pending architecture review. Do **not** work around it per-caller.
|
|
65
76
|
|
|
66
77
|
## Change history
|
|
78
|
+
- 2026-08-05 — Documented (no code change) that the `direction = 'OUT'` api-log row is
|
|
79
|
+
**committed immediately** inside `execute()` (`transactionCommit(DB_CLIENT_LOGS)`), so it
|
|
80
|
+
survives a later rollback and `Logs_<Client>.Api` is definitive evidence of whether an outbound
|
|
81
|
+
call was attempted — the first diagnostic for a silent integration. Also noted that in a bare CLI
|
|
82
|
+
script without `_Database::registerClientDatabases()`, the logging branch throws
|
|
83
|
+
`Unknown database 'ClientLogs'` out of `execute()` and masquerades as a broken integration.
|
|
84
|
+
Surfaced building `_Component_Api_Togaiq`. (bala)
|
|
67
85
|
- 2026-07-28 — Broadened from the Content-Type fix to the general `_ApiRequest` contract:
|
|
68
86
|
documented the non-assoc `json_decode` response (returns `stdClass`), the `ENCODE__JSON`
|
|
69
87
|
re-encode losing `JSON_UNESCAPED_*`, `setAutoRetry()`'s flat-delay/retry-all behavior,
|