Direct Link Resolver Modernized - sebiboga/testlink-upgraded GitHub Wiki
Modernization of the Deep-link resolver entry point (linkto.php) for
requirements β GitHub issue
#1532.
Legacy direct links (linkto.php?item=req&id=<doc_id>) used to land in a
frameset shell pointing at the legacy requirement viewer. After the ASIDE menu
was fully modernized the other remaining legacy surface was the
linkto.php deep-link gateway: bookmarks, e-mail links and "copy link" URLs
emitted by the modern requirement screens still pointed at it. This milestone
repointed the requirement deep-link family onto a modern, self-contained
resolver page (gui/templates/links/directLink.html) backed by a plain-PHP
REST BFF (api/directlink/index.php), without touching the other item types.
URL: gui/templates/links/directLink.html?tprojectPrefix=<prefix>&item=req&id=<doc_id>[&version=<n>]
BFF API: api/directlink/index.php (GET ?action=resolve)
Rights: authenticated session required; mgt_view_req on the owning project (403 otherwise)
Compatible alias: tproject_id=<numeric> may be used instead of tprojectPrefix=<prefix>



| Feature | Legacy behavior | Modern implementation |
|---|---|---|
| Entry point |
linkto.php?item=req&id=<doc_id> β frameset shell + legacy viewer |
linkto.php (outer frame and inner frame) 302-redirects item=req onto the resolver; modern screens emit the resolver URL directly (api/requirements direct_link, reqRevisionView.html copy-link) |
| Resolution | server-side frameset that dumped the id into the legacy viewer | BFF resolves doc ID β project (prefix or numeric id), requirement record, latest or requested version + revision, then the page shows a Dashio item card |
| Item card | n/a (immediate redirect) | project, requirement (doc id), title, version, revision, plus a copy-link read-only box and an Open viewer button |
| Open viewer | legacy frameset workframe URL |
reqView.html?id=<req_id>&tproject_id=<id> β the modern requirement viewer |
| Copy link | n/a | read-only input pre-filled with the resolved viewer URL + Copy button (clipboard API with fallback) |
| Version pinning | literal version passthrough to the viewer |
/api/directlink validates the exact version exists (404 on unknown version) and re-links the viewer URL to that version |
| Resolve again | n/a | re-runs the same resolution (also acts as Retry after an error) |
| Locale | per-session UI | the standard TLi18n locale switcher; page reads locale query param for a fresh explicit locale |
| Anonymous | sent to the login form | resolver detects 401 and bounces to /index.php (β login), same as legacy |
| No right | generic viewer error | 403 β localized "No permission to view requirements in this project" |
Only one route is needed. Requires a session (401 anonymous). item=req is the
only supported item type; anything else is a 400 (unknown item family kept on
the legacy linkto.php shell for backwards compatibility).
| Method & path | Purpose | Failure modes |
|---|---|---|
GET ?action=resolve&tprojectPrefix=<prefix>&item=req&id=<doc_id>[&version=<n>] |
resolve doc id to project + requirement + version/revision; returns project and item metadata plus the modern viewer URL | 400 missing/unknown params, 400 unsupported item, 401 anonymous, 403 no mgt_view_req, 404 unknown project prefix / doc id / version, 405 non-GET (same-origin guard runs first), 500 guarded |
Request may use tproject_id=<numeric> instead of tprojectPrefix (only one
of the two must be present β both resolve the same project).
200 payload shape:
{
"status": "ok",
"tproject_id": 4, "tproject_name": "DL1532", "tproject_prefix": "DLS2",
"req_id": "7", "req_doc_id": "DLREQ-001", "req_title": "β¦",
"version": "1", "version_id": 8, "revision": "1",
"href": "/gui/templates/requirements/reqView.html?id=7&tproject_id=4",
"grant": { "req_mgmt": "yes" }
}When version is requested it is validated (requirement_mgr::get_by_id,
human version number), the card metadata comes from THAT version, and
href gains &req_version_id=<version_id> so the modern viewer opens the
pinned version (request params version_id/req_version_id are honored by
reqView.html; its own "This version" direct link then round-trips back via
&version=N).
-
linkto.php?item=req&id=<doc_id>(noload) β outer frame β now 302s to the resolver.linkto.php?load&item=req&β¦(inner-facing bookmark) does the same. - Emitting new legacy linkto URLs has stopped:
api/requirementsdirect_linkandreqRevisionView.htmlcopy-link now producegui/templates/links/directLink.html?β¦URLs. - The other
item=families (reqspec,testcase,testsuite) stay behindlinkto.phpand open the legacy frame shell. Issue #1533 fixed the HTTP 500 that broke them (the removedtestproject::setSessionProject()call on the inner-frame path is replaced by direct session assignment), so those legacy deep links work again β they are not redirected to the resolver, onlyreqis (the resolver BFF supports requirements only for now). - Fixture used during testing (created by
tmp/fixtures_1532.php): test project DL1532 (prefixDLS2, id 4), requirement spec RS-DL (id 5), requirements DLREQ-001 (id 7) and DLREQ-002 (id 9).
Client-side keys live in the dl.* namespace plus footers.directLink, added
to all 10 bundles (en, ro, de, es, fr, it, ja, pt, ru,
zh), validated with python3 -m json.tool:
-
dl.resolve(headline),dl.resolveTitle(doc title / page<title>) -
dl.resolveAgain,dl.resolvedItem,dl.fieldProject,dl.fieldRequirement,dl.fieldTitle,dl.fieldVersion,dl.fieldRevision -
dl.openViewer,dl.copyLink,dl.copySuccess,dl.copyError -
dl.errTitle,dl.errBoxTitle,dl.errViewer,dl.loading(fallback text) footers.directLink
- Session-only auth (401 anonymous); the resolver page never leaves the app origin.
- Per-project right check
mgt_view_reqinside the BFF (403 for the<no rights>role, verified both via curl and in-browser). - Same-origin CSRF guard on every BFF action (shared
bffSameOriginGuard()helper); non-GET returns 405 past the guard, 403 without theX-Requested-Withproof. - Every legacy-class touch is wrapped in guarded try/catch β 500 JSON, no
leaked stack traces. No raw
$_GETinterpolation anywhere; all ids / prefixes are validated before use. - No secrets in this milestone; no new writes to the DB.
Full suite recorded in tmp/TLU_Test_Cases.md (Suite 1532), covering:
BFF contract (200 ok / 400 missing params / 400 unsupported item / 404 bad
prefix / 404 bad doc id / 404 bad version / 405 method / 401 anonymous / 403
no-right / tproject_id alias), the resolver page state changes (ok card,
not-found, no-permission), locale switch (enβro), anonymous bounce to login,
linkto.php outer+inner frame 302 redirects, api/requirements
direct_link + reqRevisionView.html copy-link emission, and Event Viewer
cleanliness. See GitHub issue
#1533 for the
pre-existing inner-frame setSessionProject fatal found while testing.
GitHub issue #1542
completed the gateway: the deep-link items test case, test suite and
requirement spec (still routed into the legacy inner-frame shell β
reqSpecListTree.php / listTestCases.php / reqSpecView.php /
archiveData.php) now also resolve through the same modern resolver page and
BFF instead of the legacy frames.
URL: gui/templates/links/directLink.html?tprojectPrefix=<prefix>&item=req|reqspec|testcase|testsuite&id=<id>
BFF API: api/directlink/index.php (GET ?action=resolve, item dispatch below)
| Item | id format | Resolution (legacy parity) | Viewer target |
|---|---|---|---|
req |
doc id (DLREQ-101) |
requirement_mgr::getByDocID |
reqView.html?id=<req_id>&tproject_id= |
reqspec |
spec doc id (RS-DL2) |
requirement_spec_mgr::getByDocID |
reqSpecView.html?id=<spec_id>&tproject_id= |
testcase |
full external id (DLS4-1) |
testcase::getInternalID(external, glue, tproject_id) |
tcView.html?tcase_id=<tcase_id>&tproject_id= |
testsuite |
numeric node id | node must be a testsuite node owned by the project |
suiteView.html?id=<suite_id>&tproject_id= |
Rights gates mirror legacy linkto.php checkTestProject(): req/reqspec
β mgt_view_req, testcase/testsuite β mgt_view_tc, checked against the
owning project (403 otherwise). The short link form ?testcase=DLS4-1 is
normalized by linkto.php init_args() and resolves identically.
The resolver card is now generic: an Item type chip (localized), type-specific identifier, title, and version/revision rows shown only when the resolved item carries them (requirement/pinned versions only).
linkto.php outer frame and inner (load) frame both 302 every supported item
type onto the resolver (anchor preserved); the legacy inner-frame
setSessionProject workaround for non-req items was removed with the legacy
shell. $actions->directLink remains the common.php wiring.
Screenshots (docs/screenshots/issue-1542-*.png): testcase card + tcView
viewer, reqSpecView viewer, suiteView viewer, not-found error card.
Testing: Suite 1542 in tmp/TLU_Test_Cases.md (13/13 PASS) β all item
types + errors (browser + curl), anonymous 401 bounce, guest 403 split
(dlguest role 5: mgt_view_tc yes / mgt_view_req no), tproject_id
alias, copy/resolve-again, locale switch, Event Viewer clean. Fixture
tmp/fixtures_1542.php (project DL1542/DLS4, spec RS-DL2, req DLREQ-101,
suites 16/17, testcase DLS4-1 id 18, guest user). Commits 46b205338 β
04b483068 β de48a7ff1.




