Requirement Viewer - sebiboga/testlink-upgraded GitHub Wiki

Requirement Viewer (reqView)

Modernized screen: Requirement Viewer (opened from Requirement Overview, Requirement Monitoring Overview and Search Requirements). Refs #755.

What it does

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
Print 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".

Version selector & monitor

  • 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/monitor action ({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_id in a popup. The print screen mirrors tcPrint.html: it calls GET /api/requirements/index.php?action=req_print and 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>, legacy reqView.php formula) and re-renders the version-specific link when the version selector changes.

New Revision (Refs #1303)

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 the mgt_modify_req grant (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.prompt with reqv.newRevisionPrompt ("Revision log message:") β€” the modern equivalent of the legacy ask4log() 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}/revision with {log_message}: re-checks mgt_modify_req on the owning project (HTTP 403), rejects frozen versions (HTTP 409), 404s on unknown version ids. It mirrors reqCommands::doCreateRevision β†’ requirement_mgr::create_new_revision: snapshots the version row into req_revisions (scope/status/type + custom fields via copy_cfields), keeps the OLD log_message in the snapshot (legacy copy_version_as_revision SELECT), then bumps req_versions.revision (+1), stamps creation_ts/author_id and stores the NEW log message.
  • Feedback: teal toast reqv.revisionCreated and the whole view reloads β€” the version selector now reads vN rM+1 and the history/compare features show the new snapshot.
  • No audit event is emitted on purpose: legacy doCreateRevision emits none and no locale key exists (a TLS() call would raise a Not-localized Event Viewer warning); the revision journal lives in req_revisions.

Edit / Delete / Copy requirement actions (Refs #1300)

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_mgmt when the displayed version is open, including an explicitly selected historical open version. It opens reqEdit.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_mgmt only on the current open version. It asks for confirmation, then calls DELETE /api/requirements/index.php/{id} to remove the complete requirement and all versions, distinct from Delete this version.
  • Copy is shown for req_mgmt only 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 calls POST /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.

Requirement Viewer with Edit, Delete and Copy actions

Freeze / Unfreeze version (Refs #1302)

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 (right mgt_unfreeze_req) gates both directions β€” the schema has no separate freeze_req right. The /view BFF grant block now exposes unfreeze_req alongside the others. Button visibility additionally requires req_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.php is_open check).
  • Confirm dialog carries the version/docId/title, matching the legacy warning_freeze_requirement / warning_unfreeze_requirement fill: "You are going to freeze version {v} - {docId}: {title}. Are you sure?".
  • Server-side write POST /api/requirements/index.php/versions/{id}/freeze and /versions/{id}/unfreeze: both 403 unless the caller has mgt_modify_req AND mgt_unfreeze_req on the version's OWNING project (legacy reqEdit.php:315 page gate + tpl grant), 404 on unknown version id. They mirror reqCommands::doFreezeVersion / doUnfreezeVersion β†’ requirement_mgr::updateOpen($versionId, false|true) (writes req_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.unfrozenOk and the view reloads β€” Frozen badge Yes, version selector shows vN rM *, and the New Revision button disappears for frozen versions.

New Version / Delete Version (Refs #1301)

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:

Requirement Viewer with New Version + Delete Version buttons (open version)

  • New Version button β€” gated ONLY on the req_mgmt grant (right mgt_modify_req on 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 opens window.prompt with reqv.newVersionPrompt ("Please add a log message") β€” the modern ask4log() β€” which may be submitted empty (legacy parity); Cancel is a clean no-op.
  • Server-side create POST /api/requirements/index.php/versions with {req_id, version_id?, log_message?, tproject_id?}: re-checks mgt_modify_req on the owning project (HTTP 403), 404 on unknown requirement, resolves the source version (explicit id belonging to the req, else the latest) and mirrors reqCommands::doCreateVersion β†’ requirement_mgr::create_new_version (copy scope/status/type/expected coverage/custom fields/attachments/TC links, stamp the log message, notify monitors). With cfg req_cfg->freezeREQVersionOnNewREQVersion enabled (config.inc.php:1428, default TRUE) the SOURCE version is auto-frozen β€” the new version selector then shows vN 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_mgmt AND 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_version from reqViewVersions.tpl:267-269). Clicking opens window.confirm with reqv.deleteVersionConfirm ("You are going to delete: Version {v} - {docId}: {title}. Are you sure?").
  • Server-side delete DELETE /api/requirements/index.php/versions/{versionId}: re-checks mgt_modify_req on 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 calls requirement_mgr::delete(req_id, version_id, user_id) and writes the legacy audit event logAuditEvent(..., '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.versionDeleted and the view reloads β€” the selector drops the deleted version (or gains vN+1), counting "Showing vN rM" reflects the reload target (highest remaining).

Access & permission

  • Deep links switched from lib/requirements/reqView.php to gui/templates/requirements/reqView.html in 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.html openReqWindow(), Refs #1477): the last legacy screen reference left in the modern UI. The linked requirements list ("LINKED REQUIREMENTS" section) now opens gui/templates/requirements/reqView.html?id=..&tproject_id=.. instead of /lib/requirements/reqView.php?showReqSpecTitle=1&requirement_id=... The legacy showReqSpecTitle=1 flag 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 of requirement_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".

i18n

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.

BFF

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

Bugs found while testing

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

Test evidence

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.

reqView toolbar with Direct link box Print screen Requirement Viewer opened from the Set Results popup Requirement Viewer after creating a revision (v1r2) Requirement Viewer with the Freeze button (open version) Requirement Viewer after freeze β€” Unfreeze button on frozen version Add link to test case modal with the legacy validation message Linked Test Cases grid with history / design / remove icons

Code review (subagent) and the fixes it produced

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.

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