Direct Link Resolver Modernized - sebiboga/testlink-upgraded GitHub Wiki

Direct-Link Resolver β€” Modernized Screen

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>


Table of Contents

  1. What the screen does
  2. REST API Reference
  3. Legacy parity notes
  4. i18n Keys
  5. Security
  6. Testing

Screenshots

Direct-Link Resolver β€” resolved requirement (English)

Direct-Link Resolver β€” Romanian locale

Direct-Link Resolver β€” no permission on the project (403)

1. What the screen does

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"

2. REST API Reference

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).

3. Legacy parity notes

  • linkto.php?item=req&id=<doc_id> (no load) β€” 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/requirements direct_link and reqRevisionView.html copy-link now produce gui/templates/links/directLink.html?… URLs.
  • The other item= families (reqspec, testcase, testsuite) stay behind linkto.php and open the legacy frame shell. Issue #1533 fixed the HTTP 500 that broke them (the removed testproject::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, only req is (the resolver BFF supports requirements only for now).
  • Fixture used during testing (created by tmp/fixtures_1532.php): test project DL1532 (prefix DLS2, id 4), requirement spec RS-DL (id 5), requirements DLREQ-001 (id 7) and DLREQ-002 (id 9).

4. i18n Keys

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

5. Security

  • Session-only auth (401 anonymous); the resolver page never leaves the app origin.
  • Per-project right check mgt_view_req inside 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 the X-Requested-With proof.
  • Every legacy-class touch is wrapped in guarded try/catch β†’ 500 JSON, no leaked stack traces. No raw $_GET interpolation anywhere; all ids / prefixes are validated before use.
  • No secrets in this milestone; no new writes to the DB.

6. Testing

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.

7. 2.0.1 update β€” all item types resolved by the resolver (Refs #1542)

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.

Screenshots (Refs #1542)

Deep-link resolver β€” test case card

Deep-link resolver β€” Test Case Viewer target

Deep-link resolver β€” Requirement Spec Viewer target

Deep-link resolver β€” Test Suite Viewer target

Deep-link resolver β€” not-found error card

⚠️ **GitHub.com Fallback** ⚠️