Requirement Export Modernized - sebiboga/testlink-upgraded GitHub Wiki
Modernization of the legacy Requirement Export screen
(lib/requirements/reqExport.php,
gui/templates/dashio/requirements/reqExport.tpl) β tracked in
#1001.
The legacy Smarty page is replaced by a standalone Dashio page
(gui/templates/requirements/reqExport.html) backed by a plain-PHP REST BFF
(api/reqexport/index.php). Every legacy entry point now opens the modern
screen instead of reqExport.php.
Path: Requirements β Requirement Specification Management β toolbar Export
(whole project), or Requirement Specification Viewer β toolbar Export /
Export branch
URL: gui/templates/requirements/reqExport.html?scope=tree|branch|items&tproject_id=..[&req_spec_id=..]
BFF API: GET ?action=options + POST ?action=export on /api/reqexport/
Rights: mgt_view_req on the target test project (same rightsAnd check as
legacy reqExport.php).

The BFF reproduces lib/requirements/reqExport.php doExport() so the
produced XML is byte-identical to the legacy download:
-
Scopes (legacy
scopeparam, defaultitems):-
treeβ every top-level requirement spec of the project (getFirstLevelInTestProject), root<requirement-specification> -
branchβ one spec + its whole sub-tree (recursive) (exportReqSpecToXML(RECURSIVE=true)), root<requirement-specification> -
itemsβ direct requirement children of one spec (non-recursive), root<requirements>
-
-
File type: XML always (
get_export_file_types()returns only XML); the legacy CSV branch is kept server-side for parity but not offered in the UI. -
Export attachments checkbox β
ATTACHMENTSoption; attachment contents are base64-encoded inside<attachments>. -
Default export file name (legacy
initializeGui()):-
treeβall-req.xml -
branchβ<spec title>-req-spec.xml -
itemsβ<spec title>-child_req.xml
-
-
GET ?action=optionsresolves the scope context β spec title, default filename, currentmgt_view_reqright (404 on unknown spec; 400 on bad id). - User edits the file name, toggles attachments, clicks Export.
-
POST ?action=exportstreams the XML asapplication/xml+Content-Disposition: attachment(filename CR/LF/quote-sanitized),Pragma: public,Cache-Control: must-revalidate. - Browser saves the file via
fetch()+ blob download; JSON errors surface as a toast.
-
reqSpecMgmt.htmltoolbar: new Export button (btnExportReqs) βreqExport.html?tproject_id=..&scope=tree; when a spec is being managed it passesreq_spec_id+scope=branch. Always visible (legacy only needsmgt_view_req, unlike Import which needs modify rights). -
reqSpecView.htmltoolbar: Export (items) and Export branch links βreqExport.html?req_spec_id=..&tproject_id=..&scope=items|branch.

- Invalid / unknown
req_spec_idβ clean 404 JSON. Before the fix the legacyrequirement_spec_mgr::get_by_id()produced broken SQL for missing ids and the DB layer aborted with a CWE-200 backtrace dump; the BFF now pre-checks the node via a flatnodes_hierarchylookup. - Invalid scope β falls back to
items; empty file name β client-side toast. - Unauthenticated β 401, missing right β 403.
-
Header injection:
Content-Dispositionfilename stripsCR/LF/"andbasename()(spaces β_) -
XSS: all DOM values escaped via
esc()/.text()/.val() -
SQL injection: ids
intval()-cast, scope/type whitelisted -
CSRF / same-origin:
bffSameOriginGuard()+X-Requested-With
While testing this screen a shared defect was found on the other modern XML
export screens (planExport.html, tcExport.html, usersExport.html): their
$.ajax({ xhrFields: { responseType: 'blob' } }) download reported
"Export failed" on every valid export β jQuery sniffed the
application/xml response, ran its xml converter over a Blob and routed to
.fail(). All four screens now download via fetch() + res.blob().
All strings use reqexp.* keys present in all ten locale bundles.
Suite 1001 in tmp/TLU_Test_Cases.md β see the suite for the executed
matrix (scopes Γ attachments Γ filename validation Γ right gate).
gui/templates/dashio/requirements/reqExport.tpl:65-69 loaded
gui/templates/conf/input_dimensions.conf and rendered the file-name field
with the budget configured there:
{config_load file="input_dimensions.conf" section=$cfg_section}
...
<input type="text" name="export_filename" id="export_filename"
maxlength="{#FILENAME_MAXLEN#}" ... size="{#FILENAME_SIZE#}"/>
gui/templates/conf/input_dimensions.conf:22-23:
FILENAME_MAXLEN=50
FILENAME_SIZE=50
That maxlength was the only enforcement point: both
lib/requirements/reqExport.php::doExport() and the BFF pass
export_filename straight into the Content-Disposition header, so any HTTP
client bypassing the browser could put an unbounded name into the header.
No limit anywhere in the modern stack β reqExport.html rendered
<input id="exportFilename" required> with no maxlength/size, and the BFF
did not cap the value either. Measured pre-fix with a 204-char name
('A'*200 + '.xml'): both the legacy page and the BFF emitted all 204
characters into the response header.
| Layer | Behaviour |
|---|---|
BFF filenameDimensions()
|
reads FILENAME_MAXLEN / FILENAME_SIZE from gui/templates/conf/input_dimensions.conf (safe default 50/50) β same pattern as api/reqtcassign scopeShortTruncate() β SCOPE_SHORT_TRUNCATE
|
BFF ?action=options
|
returns filename_maxlen and filename_size
|
BFF clampExportFilename()
|
caps the name before the header is built; counts characters (like the browser attribute), UTF-8 safe, with a byte-loop fallback if mbstring is missing |
Screen updateLimitHint()
|
applies maxlength/size from the API response (never hardcoded) and renders the live budget hint |
Screen trimToMaxlen()
|
paste / drop / autofill guard β the HTML maxlength never fires on those paths β plus a red toast |
New i18n keys reqexp.filenameLimit and reqexp.errFilenameTooLong in all
ten locale bundles (e.g. Romanian: 50 din 50 caractere,
Numele fiΘierului de export este limitat la {max} caractere).
The audit that opened #1295 was right that the screen is at parity, but its
fixture had no attachments, so exportAttachments=1 "changing nothing" was
never actually exercised. tmp/fixtures_1295.php adds real attachments to a
req spec and to a requirement; with them, legacy and modern output are
byte-identical in all three scopes, with and without the flag:
| Scope | attachments off | attachments on |
|---|---|---|
tree |
1022 B, 0 <attachment>
|
1390 B, 1 <attachment>
|
branch |
1022 B, 0 <attachment>
|
1390 B, 1 <attachment>
|
items |
471 B, 0 <attachment>
|
839 B, 1 <attachment>
|
The CSV dropdown is not a gap: requirement_spec_mgr::$export_file_types
is array("XML" => "XML"), so the legacy UI offered XML only and its
case 'csv' branch of doExport() was unreachable there too.
Suite 1295 in tmp/TLU_Test_Cases.md β 16/16 PASS (options payload, browser
typing, autofill bypass, exact-50, whitespace-only, end-to-end download,
Romanian locale, 6-combination byte-diff matrix vs legacy, clamped-content
matrix, header-name edge cases, multi-byte budget, 403 rights gate for a
<no rights> user, Event Viewer clean).
