Bugfix Issue 555 TrackerGetInterfaceObject NullGuard - sebiboga/testlink-upgraded GitHub Wiki
Issue 555 — tlIssueTracker::getInterfaceObject(): PHP 8 array-offset-on-null for orphaned tracker link
Issue: #555
Branch: fix/issue-555
Status: VERIFIED-FIXED & CLOSED (2026-08-25)
Symptom
With issue_tracker_enabled=1 on a project, if the referenced issuetrackers row is deleted
(orphaned FK — schema does not enforce foreign keys), any caller of
tlIssueTracker::getInterfaceObject() (print, execSetResults, bugAdd, mainPage, resultsBugs,
etc.) could trigger:
E_WARNING: Trying to access array offset on null
in lib/functions/tlIssueTracker.class.php - Line 690
Found during code review of #553 — the #553 fix guarded $issueT but left a downstream
dereference of $itd unguarded.
Root cause chain & fix approach (the HOW)
-
getInterfaceObject()calls$this->getLinkedTo($tprojectID)(line 672) which uses INNER JOIN onissuetrackers— returns non-null only when bothtestproject_issuetrackerandissuetrackersrows exist. -
The #553 fix (commit
4f841bb91) added null guard for$issueTat lines 675 and 687, preventing the first unguarded dereference ($issueT['issuetracker_name']). -
But at line 689:
$itd = $this->getByID($issueT['issuetracker_id'])—getByID()(line 344) queriesissuetrackerstable independently viagetByAttr()which returnsnullif no row matches. -
At line 690:
$iname = $itd['implementation']— unguarded array access on potentially null$itd→ PHP 8 E_WARNING.
The INNER JOIN in getLinkedTo() means the orphaned-link race condition is unlikely in normal
operation (the JOIN filters out missing rows). However, a race condition is possible:
getLinkedTo() succeeds (tracker exists at query time), then getByID() runs (tracker deleted
between the two queries). The unguarded dereference is a latent defect regardless.
Fix: Added is_null($itd) guard at line 689-693 returning null early when tracker data
is missing. This prevents the unguarded array access and is consistent with the existing null
guard pattern from #553.
Why this approach: minimal 4-line addition, matches the existing guard pattern, returns null
which all 14 callers of getInterfaceObject() handle gracefully (except bugAdd.php:293-302
which has a pre-existing unguarded downstream use — tracked separately). Alternatives rejected:
throwing an exception (too invasive for a defensive guard), logging and continuing (would still
crash on new $iname()).
Files changed
| File | Change |
|---|---|
lib/functions/tlIssueTracker.class.php |
Added is_null($itd) guard after getByID() at line 689, returning null early (+4 lines) |
Verification performed (2026-08-25)
Environment: fresh DB, app http://localhost:8082, admin/admin, headless Chrome, project 1
"TestProject555" with issue_tracker_enabled=1.
| Check | Result |
|---|---|
php -l lib/functions/tlIssueTracker.class.php |
No syntax errors |
Navigate to index.php?tproject_id=1 with valid tracker |
Page loads, no console errors, 0 new Event Viewer entries |
Delete issuetrackers row (orphan link), navigate to index.php?tproject_id=1 |
Page loads, no E_WARNING at line 690, 0 new Event Viewer entries |
| Code review subagent | Fix correct, no other unguarded $itd dereferences in method |
Regression suite: tmp/TLU_Test_Cases.md → "Regression — Issue #555" (2/2 PASS).