Repository navigation
Bugfix Issue 1625 ReqMgrSystemView CheckConnection 500
Bug fix — Issue #1625: reqMgrSystemView.php?id=<n> returned a blank HTTP 500 on the "Check connection" path
The legacy Dashio Req. Management System list
(lib/reqmgrsystems/reqMgrSystemView.php) returned HTTP 500 with a 0-byte body
whenever an id parameter was present, and wrote 4 PHP 8 E_WARNING rows into
the events table (the Event Viewer source) on every such request (5 when the id
does not exist — one extra array offset on null).
| Route | Pre-fix |
|---|---|
reqMgrSystemView.php?id=<existing row> ("check connection" wrench) |
500, 0 bytes, 4 event rows |
reqMgrSystemView.php?id=999 (id that does not exist) |
500, 0 bytes, 5 event rows (4 + the :619 one) |
reqMgrSystemView.php (no id) |
200, but still 2 event rows per load |
Entry point: http://localhost:8082/lib/reqmgrsystems/reqMgrSystemView.php?id=1
(the wrench icon of the first column in
gui/templates/dashio/reqmgrsystems/reqMgrSystemView.tpl:47-49 links exactly there).


-
TestLink 2.0.1, PHP 8.3 built-in server, MariaDB
testlinkon the local CI instance (127.0.0.1:3306, throwaway container credentials), loginadmin/admin. -
Pre-fix baseline commit
6d110083a; fix commitb4c325c33. -
Minimal reproduction:
INSERT INTO reqmgrsystems (name,type,cfg) VALUES ('Jira Demo',1,'{}');
type=1is the only entry oftlReqMgrSystem::$systems(lib/functions/tlReqMgrSystem.class.php:32-33:1 => contour/soap), i.e. 100 % of the rows the shipped UI can create are affected. -
Gotcha when scripting the login in 2.0.1: authentication goes through the BFF
POST /api/auth/login, which requires same-origin proof. Without-H "Origin: http://localhost:8082" -H "Referer: .../login.php"the call answers{"status":"error","message":"Forbidden: missing or mismatched same-origin proof (CSRF protection)"}and every later request returns200with a login-redirect body — which silently invalidates HTTP-only measurements.
HTTP:
?id=1 http=500 bytes=0
?id=999 http=500 bytes=0
?id=1&tproject_id=1 http=500 bytes=0
PHP level, isolated harness (config.inc.php + common.php + doDBConnect, then the
same two methods the controller calls):
getAll OK rows=1
checkConnection(1) THROWS Error: Class "contoursoapInterface" not found
checkConnection(999) THROWS Error: Class name must be a valid object or a string
Event Viewer (events table), all log_level=2, activity=PHP. 4 rows for a real
row; the 5th below appears only for ?id=999:
| description | source |
|---|---|
include_once(contoursoapInterface.class.php): Failed to open stream: No such file or directory |
lib/functions/common.php:122 |
include_once(): Failed opening 'contoursoapInterface.class.php' for inclusion (include_path=…) |
lib/functions/common.php:122 |
the same pair again (the autoloader is entered once from getAll(), once from new) |
lib/functions/common.php:122 |
Trying to access array offset on null (only for ?id=999)
|
lib/functions/tlReqMgrSystem.class.php:619 |
Browser: the document loads with no body at all (the page is blank inside the frame).
tlReqMgrSystem derives the class name of a requirement-management integration from the
row's type, and checkConnection() instantiated that name without ever checking that
it can be loaded. The chain has two independent endings, both fatal.
Hop 1 — getImplementationForType() composes a name that can never resolve
(lib/functions/tlReqMgrSystem.class.php:111-115)
$spec = $this->systems[$system]; // :112
return $spec['type'] . $spec['api'] . 'Interface'; // :114For the one shipped type this returns contoursoapInterface. The directory on the
include_path (lib/reqmgrsystemintegration/, added in cfg/const.inc.php:43-47)
contains only the abstract base reqMgrSystemInterface.class.php — the concrete
contoursoapInterface was never in this repository:
git log --all -- '*contour*' returns no commit at all. So the name is right and the
file is absent, permanently.
Hop 2 — checkConnection() instantiated it anyway (:616-622, pre-fix :616-621)
$xx = $this->getByID($systemID); // :618 -> NULL when the id does not exist
$class2create = $xx['implementation']; // :619 E_WARNING "array offset on null"
$system = new $class2create(...); // :620 uncaught Error -> HTTP 500-
?id=<real row>→Error: Class "contoursoapInterface" not found -
?id=999→getByID()isNULL→Error: Class name must be a valid object or a string
Both are Error, not Exception, so the try { … } catch (Exception $e) in the
sibling getInterfaceObject() (:588-608) never applies. PHP 8 aborts the request
before a single byte is flushed (reqMgrSystemView.php:33-35 never reached), which is
precisely why the symptom is a bare 500 with an empty body.
Hop 3 — the same screen also probes the class on every listing load (:528-537)
$impl = $this->getImplementationForType($item['type']);
if( method_exists($impl,'checkEnv') ) { … }method_exists() on an unknown class name fires the autoloader, whose include_once()
(lib/functions/common.php:122) fails and logs two events per row per page load —
the Event Viewer noise tracked in #1593, for this screen.
Why the 500 is empty. The Error is raised while the page is still being built, so
nothing has been written yet, and with display_errors=Off (the default for both FPM and
the built-in server) PHP adds nothing either — hence a 0-byte body rather than a stack
trace. On the PHP 5.x that TestLink 1.9.20 targeted the same line was an uncatchable
E_ERROR with a message when display_errors was on, which is why this was never a
blank page back then. The defect itself is pre-existing since 1.9.6, and ?id=<n> was
reachable only by hand-clicking the wrench icon.
The two twins of this code were already hardened for the very same defect and this one was not:
| Class | Guard | Issue |
|---|---|---|
tlIssueTracker::checkConnection() |
returns false before the new
|
#1617 |
tlIssueTracker::getAll() |
is_null($impl) || !@class_exists($impl) || !is_callable([$impl,'checkEnv']) |
#1617 |
tlCodeTracker::getAll() |
same shape | #1597 |
tlReqMgrSystem |
none | #1625 |
Minimal, and a deliberate port of the shape already proven twice. Diff: 2 files, +59 / −3.
-
tlReqMgrSystem::checkConnection()— two earlyreturn falseguards before thenew:if( is_null($xx) || !isset($xx['implementation']) ) { return false; } $class2create = $xx['implementation']; if( !is_string($class2create) || !@class_exists($class2create) ) { return false; }
falseis the right verdict because the whole chain already understands it:reqMgrSystemView.php:36maps it to'ko'andreqMgrSystemView.tpl:52-53draws the existing localizedreqmgrsystem_check_kobadge. -
reqMgrSystemView.php:28-37— the probe is stamped only on a row that exists ($args->id > 0 && isset($gui->items[$args->id])).$gui->itemsis keyed by row id, so without this the?id=999path would have degraded from a 500 into a phantom empty grid row (the same family as #1618 on the issue-tracker side). -
tlReqMgrSystem::getAll():528-551— the identicalis_null || !@class_exists || !is_callableguard used by both twins, so a bad row degrades toenv_check_ok = falseinstead of taking the listing down, and the twoinclude_onceevents per row per load disappear (the #1593 noise for this screen).
@class_exists() is @-silenced on purpose: the autoloader include_once()s
<class>.class.php (lib/functions/common.php:122) and an un-silenced guard would trade
a fatal for two warnings per row. is_callable() rather than method_exists() because
only a public static checkEnv can satisfy $impl::checkEnv() — a private or
non-static declaration would raise an Error and re-introduce the very fatal the guard
prevents.
No new i18n key and no bundle edit was needed: every message reuses a string that
already exists, so gui/templates/i18n/*.json was left untouched.
-
Write a
contoursoapInterface. Impossible as a restoration: the file has never existed in this repository. A SOAP stub could only ever return a fake verdict, which is worse than an honest "not connected". -
Drop
contourfrom$systems/getTypes()(option (c) in the issue body): rejected — it changes the persisted type table that existing rows and the modernized screen's dropdown depend on, to neutralise a diagnostic defect. The read-side guards fully neutralise the reported fatal. -
Catch
Throwablein the controller. Rejected: it hides the defect instead of degrading the single row, and would still emit the 5 events. -
Guard on
$typesas well as$systems. Not needed here and deliberately avoided (the #1617 code-review catch):getTypes()populates$this->typesfor enabled systems only, so an extra guard would reject legitimate disabled types.
| Route | Pre-fix | Post-fix |
|---|---|---|
?id=<existing row> |
500, 0 B, 5 event rows |
200, 10994 B, KO badge, 0 event rows
|
?id=999 |
500, 0 B, 5 event rows |
200, 10887 B, no phantom row, 0 event rows
|
no id
|
200, 2 event rows |
200, 0 event rows
|
reqMgrSystemEdit.php?doAction=checkConnection&id=<n> |
500, 0 B |
200 |
?id=abc / id=0 / id=-1
|
200 |
200, probe not run |
Full regression matrix in tmp/TLU_Test_Cases.md ("Suite 1625"), re-runnable via
bash tmp/verify_1625.sh (31 asserting HTTP/Event-Viewer/DB checks). The harness
asserts and exits non-zero on failure — proved discriminating, not self-reporting:
against the pre-fix baseline (git checkout 6d110083a -- <the two files>) it reports
16 PASS / 11 FAIL, exit 1; against the fix 31 PASS / 0 FAIL, exit 0.
Coverage also includes the regression guards: the two hardened twins
(lib/issuetrackers/issueTrackerView.php, lib/codetrackers/codeTrackerView.php — the
#1617/#1597 siblings) still answer 200 with zero new warnings, and the modernized
2.0.1 BFF api/reqmgrsystems/index.php still returns the row
({"status":"ok","items":[{… "typeDescr":"contour (Interface: soap)" …}],"total":1}).
Browser-verified for both ?id=1 and ?id=999: the grid renders (header + Jira Demo +
Create), and the browser console reports <no console messages found>.
lib/reqmgrsystems/reqMgrSystemCommands.class.php::checkConnection() — the "Check
connection" action dispatched from the edit screen — contained the identical
duplicated instantiation, unguarded, and was therefore a second, independent 500:
$ curl ... "reqMgrSystemEdit.php?doAction=checkConnection&id=1" -> http=500 bytes=0 (pre-fix)
$ curl ... "reqMgrSystemEdit.php?doAction=checkConnection&id=999" -> http=500 bytes=0 (pre-fix)
$ curl ... "reqMgrSystemEdit.php?doAction=checkConnection&id=1" -> http=200 (post-fix)
checkConnection is whitelisted in reqMgrSystemCommands.class.php:38 and dispatched by
reqMgrSystemEdit.php:22-26, so the route is live and reachable by plain URL. Rather than
guarding a second copy, the duplicate is deleted: the method now delegates to the one
hardened implementation, $this->mgr->checkConnection($argsObj->id), keeping its original
'ok' : 'ko' mapping. This is the same decision the issue-tracker twin took in #1617.
Both reproduce with valid data and neither is a regression from this diff:
-
#1626 — a
reqmgrsystemsrow whosetypeis not a key of$systemsstill logs 6E_WARNINGrows per load:Undefined array keyattlReqMgrSystem.class.php:522and:523($this->types[$item['type']]) and:113/:114($this->systems[$system]). The page is200and usable, so it is noise only; both twins already have theisset($this->types[…]) ? … : ''hardening. -
#1627 —
lib/reqmgrsystems/reqMgrSystemEdit.phpwithout (or with a non-whitelisted)doActionis a hard500:$opstaysnullandrenderGui()dereferences$opObj->template. Reachable by plain URL. -
#1628 — the
doAction=checkConnectionroute that #1625 unblocked writesE_WARNING Undefined array key "checkConnection"(3 per pass):reqMgrSystemCommands.class.php:72reads$obj->l18n[$caller]for a keyinit_labels()was never given it; the issue-tracker twin carries the missing'checkConnection' => 'btn_check_connection'entry (lib/issuetrackers/issueTrackerCommands.class.php:70-71). Revealed by this fix (the 500 used to mask it), not caused by it. -
#1629 —
tlReqMgrSystem::getInterfaceObject()is the third unguardednewin the class, and itscatch (Exception $e)cannot catch a PHP 8Error; it is called fromlib/requirements/reqSpecCommands.class.php:44for every project withreqmgr_integration_enabled. Filed as an UNVERIFIED code-path reading — the fresh CI database has no project linked to a ReqMgrSystem, so the 500 was not reproduced here.
| File | Purpose |
|---|---|
lib/functions/tlReqMgrSystem.class.php |
guards in checkConnection() and in the getAll() checkEnv block |
lib/reqmgrsystems/reqMgrSystemView.php |
probe only on a row that exists (no phantom row) |
lib/reqmgrsystems/reqMgrSystemCommands.class.php |
the edit screen's second copy of the probe now delegates instead of duplicating the unguarded new
|
tmp/verify_1625.sh |
regression harness, 31 asserting checks (force-added: tmp/ is gitignored) |
tmp/TLU_Test_Cases.md |
"Suite 1625" test cases and results |
docs/screenshots/issue-1625-reqMgrSystemView-id1-before.png |
before: blank HTTP 500 page |
docs/screenshots/issue-1625-reqMgrSystemView-id1-after.png |
after: grid rendered, row carries the KO badge |