Repository navigation
Modernize Direct Links Gateway ltxDirectLink
Issue #1677 · screen
gui/templates/links/ltxDirectLink.html· BFFapi/ltx/index.php
The repo-root gateway ltx.php was the last root-level controller still rendering a full
Smarty frameset in TestLink 2.0.1. It is now a Dashio resolver screen backed by a REST BFF,
with ltx.php kept as a session-guarded 302 shim so every existing deep link keeps working.
A two-step frameset, not a single page:
| Step | Entry | What it did |
|---|---|---|
| 1 — outer frame | ltx.php?item=… |
init_args() + checkTestPlan(), then main.tpl = navBar + asideMenu + an iframe pointing back at ltx.php&load=1
|
| 2 — inner frame | ltx.php?item=…&load=1 |
launch_inner_exec() / launch_inner_xta2m() re-resolved the context, then frmInner.tpl (exec navigator + lib/execute/execSetResults.php) or workframe.tpl (single frame) |
Three Smarty templates, two resolvable link shapes:
-
item=exec+build_id+ (feature_idortplan_id+tcversion_id) +platform_id(+anchor) -
item=xta2m+user_id+tplan_id(+build_id) — "eXecution Tasks Assigned TO Me"
Failures were reported with raw untranslated echo / die() markers LTX-01 … LTX-10.
Its already-modernized siblings — linkto.php (#1532/#1542), lnl.php (#1541),
ltcp.php (#1623) — made this the last one standing.
gui/templates/links/ltxDirectLink.html — a resolved-context card (test project, test plan,
build, platform, test case external id PREFIX-tc_external_id, suite, version) with clickable
chips to switch build, platform, or any version linked to the plan without editing the
link, a primary Open Execution and a secondary Open Navigator, and explicit
access-denied / not-found / error cards that each keep the LTX-0x marker visible.
Resolved context for an execution deep link:

Clicking a version chip and Apply re-issues the link with that version's feature_id:

Every failure state is a real screen state, not a browser alert:
| State | Card |
|---|---|
no testplan_execute right |
![]() |
| unknown plan / version / build / platform / feature | ![]() |
| unresolvable / unexpected failure | ![]() |
| full UI in Romanian (TLi18n) | ![]() |
GET api/ltx/index.php?action=init&item=exec
&build_id=N[&feature_id=M | &tplan_id=P&tcversion_id=V][&platform_id=R][&anchor=step_3]
GET api/ltx/index.php?action=init&item=xta2m&user_id=U&tplan_id=P[&build_id=N]
Session auth + bffSameOriginGuard + bffEnforceSession; the testplan_execute right is
checked on the owning project. GET only (405 otherwise). 7 stable machine codes, and every
answer also returns the legacy_code marker so each old outcome stays recognizable.
| Code | HTTP | Legacy marker | Meaning |
|---|---|---|---|
unauthenticated |
401 | — | no session (anon) |
no_rights |
403 | — | no testplan_execute on the owning project |
not_your_tasks |
403 | LTX-03 |
user_id ≠ session user |
build_id_not_set |
400 | LTX-04 |
legacy init_args gate |
security_check_ko |
400 | LTX-01 |
any item other than exec/xta2m
|
missing_user_id |
400 | LTX-01 |
xta2m without user_id
|
testplan_not_set |
400 | LTX-06 |
xta2m without tplan_id
|
plan_not_found |
404 | LTX-02 |
die("ltx - tplan info does not exist") |
unknown_build / tcversion_not_found / unknown_platform / version_not_in_plan
|
404 | LTX-09 |
the legacy die() class |
unknown_feature |
404 | LTX-10 |
null feature_id recordset |
method_not_allowed |
405 | — | non-GET |
server_error |
500 | — | DB connection failed, guarded JSON |
The legacy table also listed LTX-05 (item_not_set) and LTX-08
(platform_id_not_set), but no legacy branch ever reached them — item
was validated by the init_args switch that answers LTX-01, and a missing
platform_id is optional. They are not emulated. 19 distinct machine codes
are emitted in total.
Links now hand over to the already-modern execSetResults.html, execNavigator.html and
assignedTcOverview.html instead of the still-legacy lib/execute/*.php renderers.
ltx.php is a pure 302: session guard first, then Location: …/gui/templates/links/ltxDirectLink.html?<query>,
then exit().
- The whole query string is forwarded unchanged — including the legacy
&load=1inner-frame URL. - CR/LF are stripped from the forwarded query string (header injection).
-
testlinkInitPage($db, true)runs before the redirect, so an anonymous deep link still bounces tologin.php?note=expired&destination=…, exactly as in 1.9.20. This gateway is not public, unlikelnl.php/ltcp.php.
Each was a real hole in 1.9.20, found by porting the branch:
-
&load=1auth bypass — the inner frame re-resolved the context without the outercheckTestPlan(), so a no-rights user holding aload=1deep link reached execution. Now a single code path with the rights check always applied. -
xta2m"assigned to ME" — theuser_id != session userrejection lived only in the outer frame, so the inner frame served another user's assigned tasks. Now403 not_your_tasks. -
Cross-plan access — neither the
feature_idpath nor the explicittcversion_idpath proved the version belonged to the addressed plan. Now404 version_not_in_plan. -
NULL dereference — an unknown
feature_id/tcversion_idreadparent_idoff a null recordset (PHP 8 fatal). Now typed failures. -
Unsafe
anchor— interpolated unfiltered into the iframesrc. Now whitelisted. -
Non-numeric params —
build_id=1abcpassed the legacyif ($b_id)test because1abcis truthy in PHP. Nowintval-gated.
-
#1678 (fixed,
fc0777408) —testplan::get_parenttestsuites()recursed on a NULLparent_id, producingAND NH.id =→ MariaDB 1064 and alog_level=1 DATABASEEvent Viewer row on every hit from any screen on theget_testsuites()reporting path. Guarded withintval()+ a<= 0short-circuit; the 2.0.1nodes_hierarchyrefactor madeparent_idnullable, which makes the shape far easier to reach than in 1.9.20. -
#1679 (filed here, fixed repo-wide by a concurrent run) —
bffSameOriginGuard()short-circuited onX-Requested-Withbefore readingOrigin/Referer. Not CORS-exploitable (no BFF emitsAccess-Control-Allow-Origin), but the guard is shared by 100+ endpoints, so the fix could not be bundled into a screen commit.api/_guard.phpnow validatesOrigin/Refererfirst and demotes the XRW hint to a fallback, with default-port normalization and rejection of an unparseableOrigin.
ltx.* (53 keys) + footers.ltxDirectLink in all 10 bundles (en, ro, de, es, fr,
it, pt, ru, ja, zh), validated with python3 -m json.tool. No hardcoded strings.
The mandatory subagent review found 1 MAJOR, 14 MINOR, 14 NIT and no security hole —
it could not find a path around the version_not_in_plan proof, and rated the .fail()
handling better than execDashboard.html's. Everything actionable is fixed; the
interesting ones:
-
MAJOR — the platform selector was a silent no-op.
applySettings()always re-emitted afeature_id, and the BFF'sfeature_idbranch then overwroteplatform_idfrom thetestplan_tcversionsrow. Choosing a platform and clicking Apply snapped the context back to the feature row's platform. An explicitplatform_idnow always wins, and the feature platform is only used as a default. -
MINOR — "No platform" could never take effect. The BFF re-derived the platform from
the plan link whenever
platform_id <= 0, and the screen's "No platform"<option>hasvalue="", so the choice was indistinguishable from "the link named no platform". Both sides now use presence (isset($_GET['platform_id'])/URLSearchParams.has()) instead of truthiness — including incurrentParams(), the BFF request builder, which had the same bug and was only caught by exercising the screen in the browser. -
MINOR — an ambiguous
testplan_tcversionsrow. A version linked under several platforms has one row per platform and the old query had noORDER BY, so both the reportedfeatureid and the platform fallback came from an arbitrary row. Now ordered, and the requested platform is a preference (a version linked under platform A is still valid on platform B — that must not 404). -
MINOR — the DB was connected before the auth gate.
common.phpechoes the rawdbms_msgon a failed connect, so a down database produced an HTTP 200 carrying an untranslated host/database name. The connect moved inside thetryand behind the guard. -
MINOR — the xta2m branch forwarded
build_idunvalidated while the docs claimed membership was proven for all three link shapes. It is now proven against the owning project. -
MINOR — N+1 query and duplicate options. One extra
SELECTper linked version (2001 queries on a 2000-TC plan) and one<option>per platform link, so a multi-platform version rendered duplicate option values. Replaced by a singleLEFT JOIN+GROUP BY. -
MINOR — the header comment claimed two branches that do not exist (
LTX-05,LTX-08) and the docs listed "7 stable machine codes" where 19 are emitted. Both regenerated from the source. -
MINOR — 2 unreachable i18n keys (
ltx.errPlatformNotSet,ltx.errUnauthenticated) removed from all 10 bundles and from the screen's error map.
tmp/suite_1677.py — 96 executable cases, 96/96 PASS (exit 0): F fixture · A auth ·
E exec resolution · X xta2m · S security · O options · C contract/legacy markers · H shim ·
I i18n/wiring · V Event Viewer · R code-review regressions (18 cases, one per review
fix, so none of them can silently regress). Plus a browser pass in EN and RO with a clean
console.
Event Viewer after the whole run: 0 ERROR/WARNING rows, 0 DATABASE rows, 0
rows mentioning ltx.
Fixture: tmp/fixtures_1677.sql (project 1, plan 2 + alternate plan 12, build 8,
platforms 7/9, test case 4 with versions 5/6, feature links 10/11, no-rights user
ltxnorights). Node types are the 2.0.1 ones — testproject=1 testsuite=2 testcase=3 testcase_version=4 testplan=5 — the 1.9.20 values in the first draft are what surfaced #1678.



