Requirement Viewer - sebiboga/testlink-upgraded GitHub Wiki
Modernized screen: Requirement Viewer (opened from Requirement Overview, Requirement Monitoring Overview and Search Requirements). Refs #755.
Read-only detail view of a single requirement β the Dashio rewrite of the
legacy popup lib/requirements/reqView.php, backed by a new JSON BFF action
GET /api/requirements/view. Nothing from the legacy screen was dropped:
| Section | Content |
|---|---|
| Toolbar | version selector (vN rM, closed versions tagged *), refresh, New Revision, Create a new version, Delete this version, Edit, Delete, Copy, Freeze this version / Unfreeze this version, monitoring toggle, Print, Direct link (toggle), Help
|
opens the new print screen printReq.html in a resizable popup |
|
| Direct link | shows the requirement permalink (linkto.php?tprojectPrefix=<T>&item=req&id=<docID>) plus a version-specific link and a Copy button (clipboard API with execCommand fallback; teal toast on success) |
| Help | links to the GitHub wiki Requirement-Viewer page |
| Overview | identifier (docID), requirement title, requirement spec path, type, status, frozen, author, created / modified, coverage pct% (covered/expected)
|
| Scope | approval-scope note text (legacy "approval scope" field) |
| Custom fields | values of requirement-level custom fields (dates localized by the BFF) |
| Linked Test Cases | DataTables grid: TC external id, test case (legacy label PREFIX-ID : name, opens the modern tcView.html), TC version and an Actions column with the execution-history icon, the design icon and β when the req_tcase_link_management right is granted and the link is deletable β the remove-link icon. An Add link to test case button (same grant + latest requirement version) opens a modal with the test case external id |
| Relations | DataTables grid: relation, target requirement (docID : title, clickable), project, status |
| Monitors | DataTables grid: user login of every user monitoring the requirement (gated on monitor_requirement right; shows empty-state message when no monitors) |
Deleted requirements show a "This requirement no longer exists" banner and an empty version selector; the permission-denied path shows "Failed to load requirement: No permission".
- The selector lists every version of the requirement (newest first); a
trailing
*marks closed (frozen) versions. Switching versions reloads the whole view for that version. -
Start/Stop monitoring hits the existing
POST /api/requirements/monitoraction ({reqId, action:'on'|'off'}). Monitoring is per requirement (legacy parity), so the state survives version switches. - The Print toolbar button opens
gui/templates/requirements/printReq.html?req_id&req_version_id&req_revision&tproject_idin a popup. The print screen mirrorstcPrint.html: it callsGET /api/requirements/index.php?action=req_printand embeds the legacy generator output (lib/requirements/reqPrint.php) in a sandboxed iframe (Print / Back / Refresh toolbar, inline-anchor navigation, localized error banner on failure). The BFF resolves the requested revision (falling back to the version's stored revision) β without it the legacy generator crashed with a DB error. - The Direct link toggle shows the requirement permalink
(
basehref+linkto.php?tprojectPrefix=<code>&item=req&id=<docID>, legacyreqView.phpformula) and re-renders the version-specific link when the version selector changes.
Port of the legacy reqViewVersionsViewer.tpl "New Revision" button
(doAction=doCreateRevision) into the modern toolbar:
-
Button visibility mirrors the legacy gates exactly
(
reqViewVersionsViewer.tpl:49,62,106-111): shown only when the caller has themgt_modify_reqgrant (req_mgmt) on the requirement's own test project AND the displayed version is open (is_open = 1, i.e. not frozen). -
Log prompt: clicking opens
window.promptwithreqv.newRevisionPrompt("Revision log message:") β the modern equivalent of the legacyask4log()callback. Cancel is a clean no-op; the prompt may be submitted empty (legacy parity). -
Server-side write
POST /api/requirements/index.php/versions/{id}/revisionwith{log_message}: re-checksmgt_modify_reqon the owning project (HTTP 403), rejects frozen versions (HTTP 409), 404s on unknown version ids. It mirrorsreqCommands::doCreateRevisionβrequirement_mgr::create_new_revision: snapshots the version row intoreq_revisions(scope/status/type + custom fields viacopy_cfields), keeps the OLDlog_messagein the snapshot (legacycopy_version_as_revisionSELECT), then bumpsreq_versions.revision(+1), stampscreation_ts/author_idand stores the NEW log message. -
Feedback: teal toast
reqv.revisionCreatedand the whole view reloads β the version selector now readsvN rM+1and the history/compare features show the new snapshot. - No audit event is emitted on purpose: legacy
doCreateRevisionemits none and no locale key exists (aTLS()call would raise a Not-localized Event Viewer warning); the revision journal lives inreq_revisions.
The viewer now restores the three legacy requirement-management actions that were missing from the modern toolbar:
-
Edit is shown for a caller with
req_mgmtwhen the displayed version is open, including an explicitly selected historical open version. It opensreqEdit.html?id=<requirement_id>&tproject_id=<project>&version_id=<version_id>; the editor and save request use that exact database version id. -
Delete is shown for
req_mgmtonly on the current open version. It asks for confirmation, then callsDELETE /api/requirements/index.php/{id}to remove the complete requirement and all versions, distinct from Delete this version. -
Copy is shown for
req_mgmtonly on the current version and opens the destination-selection screen with the source requirement preselected. It is not the Direct link clipboard Copy button. The single-requirement screen callsPOST /api/requirements/index.php/copy; the existing specification copy flow remains available for multi-requirement copies.
The BFF rechecks authentication, project ownership and req_mgmt on every
write. Copy propagates the test-case-assignment option and records the legacy
COPY audit event; full deletion notifies monitors, uses the
ALL_VERSIONS sentinel and records the DELETE audit event. Unknown resources
return structured JSON 404 responses.

Port of the legacy reqViewVersionsViewer.tpl "Freeze this version" /
"Unfreeze this version" buttons (doAction=doFreezeVersion|doUnfreezeVersion)
into the modern toolbar:
-
Grants mirror the legacy exactly: a SINGLE grant
unfreeze_req(rightmgt_unfreeze_req) gates both directions β the schema has no separatefreeze_reqright. The/viewBFF grant block now exposesunfreeze_reqalongside the others. Button visibility additionally requiresreq_mgmt, because legacy renders the whole buttons form only inside{if $args_grants->req_mgmt == "yes"}(tpl:49,86-98). -
State gating: Freeze renders only for open versions, Unfreeze only
for frozen ones (legacy
reqView.phpis_opencheck). -
Confirm dialog carries the version/docId/title, matching the legacy
warning_freeze_requirement/warning_unfreeze_requirementfill: "You are going to freeze version {v} - {docId}: {title}. Are you sure?". -
Server-side write
POST /api/requirements/index.php/versions/{id}/freezeand/versions/{id}/unfreeze: both 403 unless the caller hasmgt_modify_reqANDmgt_unfreeze_reqon the version's OWNING project (legacy reqEdit.php:315 page gate + tpl grant), 404 on unknown version id. They mirrorreqCommands::doFreezeVersion/doUnfreezeVersionβrequirement_mgr::updateOpen($versionId, false|true)(writesreq_versions.is_open: 0 = frozen, 1 = open). Idempotent per legacy: no 409 state pre-check. -
Audit events (legacy parity):
logAuditEvent(..., 'FREEZE'|'UNFREEZE', $versionId, 'req_version')with the literal legacy wording " Version {v} of Req 'DOCID:{docId}' - {title} was frozen/unfrozen.". -
Feedback: teal toast
reqv.frozenOk/reqv.unfrozenOkand the view reloads β Frozen badge Yes, version selector showsvN rM *, and the New Revision button disappears for frozen versions.
Port of the legacy reqViewVersionsViewer.tpl "Create a new version" (tpl:113-
114) and "Delete this version" (tpl:76-81) buttons (doAction=doCreateVersion /
doDeleteVersion, reqCommands.class.php:609,632) into the modern toolbar,
placed next to the New Revision button:

-
New Version button β gated ONLY on the
req_mgmtgrant (rightmgt_modify_reqon the requirement's own test project). Legacy renders it inside the req_mgmt form but NOT on the frozen state, so it stays available even for FROZEN versions. Clicking openswindow.promptwithreqv.newVersionPrompt("Please add a log message") β the modernask4log()β which may be submitted empty (legacy parity); Cancel is a clean no-op. -
Server-side create
POST /api/requirements/index.php/versionswith{req_id, version_id?, log_message?, tproject_id?}: re-checksmgt_modify_reqon the owning project (HTTP 403), 404 on unknown requirement, resolves the source version (explicit id belonging to the req, else the latest) and mirrorsreqCommands::doCreateVersionβrequirement_mgr::create_new_version(copy scope/status/type/expected coverage/custom fields/attachments/TC links, stamp the log message, notify monitors). Withcfg req_cfg->freezeREQVersionOnNewREQVersionenabled (config.inc.php:1428, default TRUE) the SOURCE version is auto-frozen β the new version selector then showsvN rM *for the source. Next version number is always last + 1. Response{status:'ok', req_id, version_id, version, source_version_id, source_frozen}. No audit event β legacy emits none (parity). -
Delete Version button β shown ONLY when
grant.req_mgmtAND the current version is NOT frozen AND more than one version exists (exact legacy gate: req_mgmt form tpl:49, frozen check tpl:62,my_delete_versionfromreqViewVersions.tpl:267-269). Clicking openswindow.confirmwithreqv.deleteVersionConfirm("You are going to delete: Version {v} - {docId}: {title}. Are you sure?"). -
Server-side delete
DELETE /api/requirements/index.php/versions/{versionId}: re-checksmgt_modify_reqon the owning project (HTTP 403), 404 on unknown version, HTTP 409 when the version is the ONLY one (requirement_mgr::delete()would otherwise full-delete the requirement). It notifies monitors (setNotifyOn delete) then callsrequirement_mgr::delete(req_id, version_id, user_id)and writes the legacy audit eventlogAuditEvent(..., 'DELETE', $versionId, 'req_version')β wording " Version {v} of Req 'DOCID: {doc}' - {title} was deleted." (locale/en_US/strings.txt:3375). -
Feedback: teal toast
reqv.versionCreated/reqv.versionDeletedand the view reloads β the selector drops the deleted version (or gains vN+1), counting "Showing vN rM" reflects the reload target (highest remaining).
- Deep links switched from
lib/requirements/reqView.phptogui/templates/requirements/reqView.htmlin the three modern screens:reqOverview.html(openReq),reqMonitorOverview.html(openReq),searchReq.html(openReq/openReqVersion; the legacy revision popup is mapped to the version viewer). -
Set Results popup (
execSetResults.htmlopenReqWindow(), Refs #1477): the last legacy screen reference left in the modern UI. The linked requirements list ("LINKED REQUIREMENTS" section) now opensgui/templates/requirements/reqView.html?id=..&tproject_id=..instead of/lib/requirements/reqView.php?showReqSpecTitle=1&requirement_id=... The legacyshowReqSpecTitle=1flag is obsolete β the modern viewer always renders the requirement spec path (r.spec_path) in the Overview card. - Required right:
mgt_view_req. The BFF resolves the requirement's own test project through a direct JOIN (requirementsβreq_specs) instead ofrequirement_mgr::getTestProjectID()(that helper throws a DB error on missing ids). Without the right the API answers HTTP 403 and the screen shows "No permission".
29 new keys (reqv.*) plus 18 more (reqv.print, reqv.directLink,
reqv.help, reqv.helpTooltip, reqv.copyLink, reqv.specificLink,
reqv.directLinkCopied and the reqprint.* block) added to all locale
bundles: en, de, es, fr, it, ja, pt, ro, ru, zh. The New Revision port adds
3 more keys to every bundle: reqv.newRevision, reqv.newRevisionPrompt,
reqv.revisionCreated. The Freeze/Unfreeze port adds 6 more keys to every
bundle: reqv.freeze, reqv.unfreeze, reqv.freezeConfirm,
reqv.unfreezeConfirm, reqv.frozenOk, reqv.unfrozenOk. The Edit/Delete/Copy
port adds reqv.deleteConfirm and reqv.requirementDeleted to all 10 bundles;
the existing common.edit, common.delete and rco.copy keys are reused for
the action labels.
The coverage link management port (Refs #1299) adds 16 more keys to all 10 bundles: reqv.actions, reqv.addLinkToTestCase, reqv.design, reqv.executionHistory, reqv.obsolete, reqv.removeLinkToTestCase, reqv.removeLinkConfirm, reqv.tcIdentity, reqv.tcIdentityHelp, reqv.linkAdded, reqv.linkRemoved and the four validation messages reqv.errFullExternalId, reqv.errOtherProject, reqv.errTcaseMissing, reqv.errLinkExecuted, reqv.errNotLatestVersion. TLi18n.has() was added to gui/templates/i18n/i18n.js so a screen can fall back to the server-rendered legacy message when a key is missing.
POST /api/requirements/index.php/coverage β add a test case link (Refs #1299)
Body {req_id, version_id, tcaseIdentity, tproject_id?}; mirrors
reqCommands::addTestCase() (lib/requirements/reqCommands.class.php:891-955).
Checks the req_tcase_link_management grant on the requirement's own project
(HTTP 403), the version ownership (400) and can_manage_coverage = the displayed
version is the latest one (409, message_key reqv.errNotLatestVersion). Then it
requires a full external id (reqv.errFullExternalId), enforces the project
prefix (reqv.errOtherProject), rejects unknown test cases
(reqv.errTcaseMissing) and, when testcase_cfg.reqLinkingDisabledAfterExec is
on, rejects test cases whose latest version was already executed
(reqv.errLinkExecuted). The link is written with
requirement_mgr::assign_to_tcase() (latest active tcversion β latest
requirement version) including the legacy audit event. Returns
{status:'ok', action:'add', req_id, req_version_id, tcase_id, tcaseIdentity}.
DELETE /api/requirements/index.php/coverage β remove a test case link
Body {req_id, version_id, tcversion_id}. The identity is the legacy one,
(req_version_id, tcversion_id), not tcase_id: a test case can have several
linked versions. The server re-checks can_be_deleted (409 Link cannot be deleted) and deletes through
requirement_mgr::delReqVersionTCVersionLink() (audit event included). Returns
{status:'ok', action:'remove', req_version_id, tcversion_id}.
GET /view additionally returns coverage[].can_be_deleted (the legacy remove
gate), can_manage_coverage, glue_char and piece_sep.
GET /api/requirements/index.php/view?id=N[&version_id=M]
Returns the requirement (current or requested version), spec path, type/status
labels, coverage data, custom-field values, monitor state for the caller,
relations, the full version list for the selector, the caller's grants
(req_mgmt, monitor_req, req_tcase_link_management), and the direct_link
permalink. Unknown ids return req_deleted:true.
GET /api/requirements/index.php?action=req_print&req_id&req_version_id&req_revision&tproject_id
Renders the single requirement via the legacy reqPrint.php into
{status, req_id, req_version_id, req_revision, tproject_id, tproject_name, reqname, title, body_html}. Checks mgt_view_req (HTTP 403 on missing right).
POST /api/requirements/index.php/versions/{id}/revision with body {"log_message":"..."}
Creates a new revision of the requirement version {id} (Refs #1303). Checks
mgt_modify_req on the owning project (HTTP 403), rejects frozen versions
(HTTP 409), 404s on missing versions. Returns
{status:'ok', req_id, version_id, revision, log_message}.
POST /api/requirements/index.php/versions/{id}/freeze and
/versions/{id}/unfreeze (Refs #1302)
Freeze/unfreeze the requirement version {id}. Checks mgt_modify_req AND
mgt_unfreeze_req on the owning project (HTTP 403), 404s on missing versions.
Returns {status:'ok', req_id, version_id, is_open, frozen} and fires a
FREEZE/UNFREEZE audit event on the req_version object.
POST /api/requirements/index.php/versions (Refs #1301)
Create a NEW version of a requirement (New Version button). Body
{req_id, version_id?, log_message?, tproject_id?}. Checks mgt_modify_req
on the owning project (HTTP 403), 404s on unknown requirement/source version,
mirrors requirement_mgr::create_new_version (copy source incl. cfields +
attachments + TC links, stamp log message, notify monitors, auto-freeze source
when cfg freezeREQVersionOnNewREQVersion is on). Returns {status:'ok', req_id, version_id, version, source_version_id, source_frozen}. No audit
event (legacy parity).
DELETE /api/requirements/index.php/versions/{versionId} (Refs #1301)
Delete ONE version (Delete this version button). Checks mgt_modify_req on
the owning project (HTTP 403), 404s on unknown version, HTTP 409 when it is
the ONLY version. Notifies monitors then requirement_mgr::delete; writes the
legacy audit event "Version {v} of Req 'DOCID:{doc}' - {title} was deleted."
Returns {status:'ok', req_id, version_id, version, remaining_versions}.
POST /api/requirements/index.php/copy (Refs #1300)
Copies one requirement to a target requirement specification. Body:
{req_id, container_id, copy_testcase_assignment}. The route authenticates the
caller, resolves the source and target specifications in the same project,
requires req_mgmt, invokes requirement_mgr::copy_to() with the legacy
copy_also.testcase_assignment option and writes the legacy COPY audit event.
Returns {status:'ok', id, message, audit_message} or structured errors.
DELETE /api/requirements/index.php/{requirementId} (Refs #1300)
Deletes the complete requirement and all versions. The route re-checks project
ownership and req_mgmt, calls the legacy deletion path with ALL_VERSIONS,
notifies monitors and writes the DELETE audit event. It is intentionally
separate from DELETE /api/requirements/index.php/versions/{versionId}.
- #765 β legacy
requirement_mgr::getTestProjectID()+requirement_spec_mgr::get_last_child_info()emit DB-SQL-error / null-array warnings when creating requirements on a first-level spec (surfaced while seeding fixtures; the modern viewer does not use this path).
Suite 764 in tmp/TLU_Test_Cases.md β 17/17 PASS (BFF routes, version switch,
monitor on/off + DB rows, deleted banner, 403 permission path, relations grid,
deep-link regression). Suite 1305 β Print / Direct link / Help (see below).
Suite 1303 β New Revision button/frozen/403/404/Event-Viewer β 7/7 PASS.
Suite 1302 β Freeze button gating / freeze+unfreeze roundtrip / confirm
dialog / 403 no-rights / 400-404 / i18n+Event-Viewer β 6/6 PASS.
Suite 1301 β New Version / Delete Version buttons, prompt + confirm dialogs,
createβauto-freeze-source roundtrip, deleteβtoast, frozen/single-version
gating, 409 last-version guard, no-rights 403, Event-Viewer + console
hygiene β 10/10 PASS.
Suite 1300 β Edit / Delete / Copy actions, exact-version edit save, frozen and
historical-version gates, single-requirement destination copy, full deletion,
invalid-id 404s, syntax/i18n/Event-Viewer hygiene β 7/7 PASS.
Suite 1299 β add/remove test case links, exec-history + design icons, obsolete
tooltip, add gating on the latest version, can_be_deleted gate on remove,
legacy validation messages, rights gate (no rights / mgt_view_req only) β 13/13 PASS.

A review of api/requirements/index.php, gui/templates/requirements/reqView.html and
gui/templates/i18n/i18n.js found no blocker (no SQL injection - all ids intval()ed,
bffSameOriginGuard() covers POST+DELETE, the rights check derives the project from the
requirement and ignores the client-supplied tproject_id) and confirmed the legacy
identity, the latest-version gate and the add validation line-for-line. Fixed:
| Severity | Finding | Fix |
|---|---|---|
| MAJOR | the test case external id was interpolated into a single-quoted onclick argument, where HTML entity escaping does not survive the HTML parser (latent stored XSS, currently unreachable because the column is int unsigned) |
the icon now carries data-cov-ext and the handler takes the element: onclick="removeCoverageLink(this, 11)" (reqView.html:691-699,788) |
| MAJOR |
assign_to_tcase() returns 0 when it inserts nothing, but the route always answered ok -> a false "link added" toast |
the return value is checked; the link is then re-read and only a genuinely missing link fails with reqv.errLinkFailed (already-linked stays a silent no-op) (index.php:900-921) |
| MAJOR | the four DELETE/403 error paths returned no message_key and the client surfaced them with a native alert() in English |
added reqv.errLinkNotDeletable, reqv.errLinkNotFound, reqv.errLinkFailed, reqv.errNoPermission to all 10 bundles; the client renders them with showToast() + covMessage() (index.php:801,832,839, reqView.html:788-812) |
| MINOR | duplicated req_version_id nesting made the DELETE fallback dead code |
collapsed to $body['version_id'] ?? $body['req_version_id'] ?? 0
|
| MINOR |
intval($lastVersion['id']) would warn on a requirement without versions; $lastVersion was computed for the DELETE path too |
?? 0 guard, and the call moved into the add branch |
| MINOR | the version guard ran after get_node_hierarchy_info()
|
guard hoisted above the call |
| MINOR |
/view derived latest_version_id from MAX(version) while the gate uses get_last_version_info(), so a version-number tie could show a button the server rejects |
/view now asks get_last_version_info() as well |
| MINOR |
$& / $` in a typed value were expanded as replacement patterns in TLi18n.t()
|
str.replace() now takes a replacement function (i18n.js:181-183) |
| MINOR | dead #covFeedback node, unused tproject_id in the add body, no in-flight guard on remove, no Enter submit |
node removed, field dropped, covBusy flag + disabled class added, keydown binding added |
Post-review re-verification (5 extra cases, tmp/TLU_Test_Cases.md 1299.4) - all PASS;
php -l, node --check and python3 -m json.tool clean; no new Error/Warning in the
Event Viewer.