Skip to content

refactor(modulesadmin): replace $myts->htmlSpecialChars() with native…#1689

Merged
mambax7 merged 5 commits intoXOOPS:masterfrom
mambax7:fix/issue-869-htmlspecialchars-modulesadmin
Apr 7, 2026
Merged

refactor(modulesadmin): replace $myts->htmlSpecialChars() with native…#1689
mambax7 merged 5 commits intoXOOPS:masterfrom
mambax7:fix/issue-869-htmlspecialchars-modulesadmin

Conversation

@mambax7
Copy link
Copy Markdown
Collaborator

@mambax7 mambax7 commented Apr 7, 2026

… htmlspecialchars()

Replace 7 occurrences of $myts->htmlSpecialChars() with native
htmlspecialchars($x, ENT_QUOTES | ENT_HTML5, 'UTF-8') in the module
admin pages where the input is structurally constrained to identifier-
like values (directory names, module display names, author strings)
that cannot contain pre-existing HTML entities.

The MyTextSanitizer wrapper has a post-processing step that un-encodes
& back to & — designed for free-form text that may already contain
HTML entities from the database. These 7 call sites handle module
dirnames and metadata where that & pass-through is unnecessary and
the standard PHP function provides correct, simpler escaping.

Also removes the now-unused $myts = \MyTextSanitizer::getInstance()
initialization from both files, and updates the source-asserting test
to match the new pattern.

Closes #869 (partial — only the identifier-constrained call sites;
the remaining ~78 occurrences depend on the & pass-through contract
and cannot be safely replaced without a broader data-model migration).

Files changed:

  • htdocs/modules/system/admin/modulesadmin/main.php (5 replacements)
  • htdocs/modules/system/admin/modulesadmin/modulesadmin.php (2 replacements)
  • tests/unit/.../MainControllerTest.php (test updated)

Full suite: 6040 tests, 10527 assertions, 0 failures.

Summary by CodeRabbit

  • Refactor

    • Standardized display escaping for module names and author info to use consistent UTF‑8-safe HTML escaping; module identifiers are no longer escaped before lookup to preserve correct install/update/uninstall behavior.
  • Tests

    • Updated unit tests to verify the new display-only escaping and that identifiers remain unescaped prior to lookups.
  • Chores

    • Updated static-analysis setup and added constant stubs for tooling.

… htmlspecialchars()

  Replace 7 occurrences of $myts->htmlSpecialChars() with native
  htmlspecialchars($x, ENT_QUOTES | ENT_HTML5, 'UTF-8') in the module
  admin pages where the input is structurally constrained to identifier-
  like values (directory names, module display names, author strings)
  that cannot contain pre-existing HTML entities.

  The MyTextSanitizer wrapper has a post-processing step that un-encodes
  & back to & — designed for free-form text that may already contain
  HTML entities from the database. These 7 call sites handle module
  dirnames and metadata where that & pass-through is unnecessary and
  the standard PHP function provides correct, simpler escaping.

  Also removes the now-unused $myts = \MyTextSanitizer::getInstance()
  initialization from both files, and updates the source-asserting test
  to match the new pattern.

  Closes XOOPS#869 (partial — only the identifier-constrained call sites;
  the remaining ~78 occurrences depend on the & pass-through contract
  and cannot be safely replaced without a broader data-model migration).

  Files changed:
  - htdocs/modules/system/admin/modulesadmin/main.php (5 replacements)
  - htdocs/modules/system/admin/modulesadmin/modulesadmin.php (2 replacements)
  - tests/unit/.../MainControllerTest.php (test updated)

  Full suite: 6040 tests, 10527 assertions, 0 failures.
Copilot AI review requested due to automatic review settings April 7, 2026 07:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f7b4fd2d-38ce-4e1c-9cec-bc19913cced0

📥 Commits

Reviewing files that changed from the base of the PR and between 63e8ba9 and a07d75d.

📒 Files selected for processing (1)
  • htdocs/modules/system/admin/modulesadmin/modulesadmin.php

Walkthrough

Replaced MyTextSanitizer htmlSpecialChars usage with explicit htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8') for display output, stopped pre-escaping module dirnames before DB/filesystem lookups, updated unit tests to assert these behaviors, and added a PHPStan constants stub with config change.

Changes

Cohort / File(s) Summary
Module admin controller
htdocs/modules/system/admin/modulesadmin/main.php
Removed \MyTextSanitizer::getInstance() and replaced $myts->htmlSpecialChars(...) with `htmlspecialchars(..., ENT_QUOTES
Module admin operations
htdocs/modules/system/admin/modulesadmin/modulesadmin.php
Stopped escaping $dirname prior to handler/DB lookup; replaced sanitizer output calls with `htmlspecialchars(..., ENT_QUOTES
Unit tests
tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php
Replaced test expecting MyTextSanitizer with testUsesHtmlspecialcharsForDisplayOnly() and added testDirnamesNotEscapedBeforeDbLookup(); assertions require full `htmlspecialchars(..., ENT_QUOTES
Static analysis config & stubs
phpstan.neon, phpstan-constants.stub.php
Replaced bootstrapFiles with stubFiles in phpstan.neon and added phpstan-constants.stub.php defining XOOPS core constants for PHPStan analysis.

Sequence Diagram(s)

(Skipped — changes are localized sanitization and test/config updates without a multi-component sequential flow needing visualization.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ggoffy
  • GregMage
  • kevinpetit
  • tad0616
  • ForXoops
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing MyTextSanitizer::htmlSpecialChars() with native htmlspecialchars() in the modulesadmin module.
Linked Issues check ✅ Passed The PR directly addresses issue #869 by replacing simple $myts->htmlSpecialChars() calls with native htmlspecialchars($x, ENT_QUOTES | ENT_HTML5, 'UTF-8') in identifier-constrained contexts.
Out of Scope Changes check ✅ Passed The phpstan.neon and phpstan-constants.stub.php changes are infrastructure improvements to support PHPStan analysis; the test updates align with the refactoring. All changes support the main objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.46)

Invalid entry in excludePaths:
Path "/tests_codex2" is neither a directory, nor a file path, nor a fnmatch pattern.

If the excluded path can sometimes exist, append (?)
to its config entry to mark it as optional. Example:

parameters:
excludePaths:
analyseAndScan:
- tests_codex2 (?)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 0% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.22%. Comparing base (f23a9d6) to head (a07d75d).
⚠️ Report is 26 commits behind head on master.

Files with missing lines Patch % Lines
...modules/system/admin/modulesadmin/modulesadmin.php 0.00% 3 Missing ⚠️
htdocs/modules/system/admin/modulesadmin/main.php 0.00% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #1689       +/-   ##
=============================================
+ Coverage          0   19.22%   +19.22%     
- Complexity        0     7584     +7584     
=============================================
  Files             0      621      +621     
  Lines             0    40085    +40085     
=============================================
+ Hits              0     7707     +7707     
- Misses            0    32378    +32378     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors parts of the System “modulesadmin” admin flow to replace MyTextSanitizer::htmlSpecialChars() with native htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8'), and updates a unit test to reflect the new escaping approach.

Changes:

  • Replace several $myts->htmlSpecialChars() calls with htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8') in modules admin pages.
  • Remove now-unused MyTextSanitizer::getInstance() initialization.
  • Update MainControllerTest to assert the new escaping approach is used.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
htdocs/modules/system/admin/modulesadmin/main.php Swaps sanitizer calls to htmlspecialchars() for module names/dirnames used during confirm/install/uninstall/update flows.
htdocs/modules/system/admin/modulesadmin/modulesadmin.php Replaces escaping for $dirname and module author output with htmlspecialchars(), and removes sanitizer initialization.
tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php Renames/adjusts a test to look for htmlspecialchars() usage instead of MyTextSanitizer usage.

$myts = \MyTextSanitizer::getInstance();

$dirname = $myts->htmlSpecialChars(trim($dirname));
$dirname = htmlspecialchars(trim($dirname), ENT_QUOTES | ENT_HTML5, 'UTF-8');
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$dirname is a module identifier used for DB lookup, cache clearing, filesystem paths, and include_once later in this function. HTML-escaping it here can change the identifier (notably converting & to &) and break module updates/paths. Keep $dirname as a validated/raw dirname for internal operations (e.g., basename() + a whitelist regex), and only apply htmlspecialchars() at the point of HTML output.

Suggested change
$dirname = htmlspecialchars(trim($dirname), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$dirname = basename($dirname);
if ('' === $dirname || !preg_match('/\A[a-zA-Z0-9_\-]+\z/', $dirname)) {
trigger_error('Invalid module dirname: ' . basename($dirname), E_USER_WARNING);
return '';
}

Copilot uses AI. Check for mistakes.
Comment on lines 314 to 318
case 'install':
$module = Request::getString('module', '');
$module = $myts->htmlSpecialChars($module);
$module = htmlspecialchars($module, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Get module handler
/** @var XoopsModuleHandler $module_handler */
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$module is used as a module dirname for loadInfoAsVar() / getByDirname() and is also passed to xoops_confirm() (which already escapes hidden values). HTML-escaping it here can change the identifier (e.g., & -> &) and can also lead to double-escaped hidden fields. Prefer validating/normalizing the dirname (e.g., basename() + allowed-char regex or membership in XoopsLists::getModulesList() like the *_ok branches) and only escape when interpolating into HTML output.

Copilot uses AI. Check for mistakes.
Comment on lines 226 to 230
$this->assertStringContainsString(
'$myts = \MyTextSanitizer::getInstance();',
'htmlspecialchars(',
$this->sourceCode,
'Should initialize MyTextSanitizer'
);

// Should use htmlSpecialChars for output
$this->assertStringContainsString(
'$myts->htmlSpecialChars',
$this->sourceCode,
'Should use htmlSpecialChars for output sanitization'
'Should use htmlspecialchars for output sanitization'
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is very broad ('htmlspecialchars(' anywhere in the source) and can pass even if the specific output-escaping behavior regresses or the required flags/encoding change. Consider asserting a more specific pattern (e.g., presence of ENT_QUOTES | ENT_HTML5 and 'UTF-8' at the relevant call sites) or checking that the old $myts->htmlSpecialChars usage is absent, to keep the test meaningful.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
htdocs/modules/system/admin/modulesadmin/main.php (3)

376-383: ⚠️ Potential issue | 🟠 Major

Same issue — HTML-escaping before database lookup.

Line 378 escapes $module before it's passed to getByDirname() on line 383. Per the relevant code snippet from htdocs/kernel/module.php:815-834, getByDirname() performs a SQL lookup using the dirname value. An HTML-escaped dirname like foo&bar won't match the raw value foo&bar stored in the database.

🐛 Proposed fix
     case 'uninstall':
         $module = Request::getString('module', '');
-        $module = htmlspecialchars($module, ENT_QUOTES | ENT_HTML5, 'UTF-8');
         $msgs = '';
         // Get module handler
         /** `@var` XoopsModuleHandler $module_handler */
         $module_handler = xoops_getHandler('module');
         $mod            = $module_handler->getByDirname($module);

Escape only when outputting to HTML context (in $msgs construction and xoops_confirm()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/main.php` around lines 376 - 383,
The code HTML-escapes $module before calling
$module_handler->getByDirname($module), which breaks the DB lookup; revert so
getByDirname() receives the raw dirname from Request::getString('module', '')
(remove or move the htmlspecialchars call), then apply htmlspecialchars only
when building output ($msgs and in xoops_confirm() calls) so user-facing HTML is
escaped but database lookups use the original $module value.

439-445: ⚠️ Potential issue | 🟠 Major

Same issue — update case also escapes before getByDirname().

Consistent with the install and uninstall cases, the dirname is HTML-escaped before the database lookup. This will fail for any module dirname containing special characters.

🐛 Proposed fix
     case 'update':
         $module = Request::getString('module', '');
-        $module = htmlspecialchars($module, ENT_QUOTES | ENT_HTML5, 'UTF-8');
         // Get module handler
         /** `@var` XoopsModuleHandler $module_handler */
         $module_handler = xoops_getHandler('module');
         $mod            = $module_handler->getByDirname($module);

Apply escaping only at HTML output points.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/main.php` around lines 439 - 445,
The update case is escaping the module dirname before the DB lookup (variable
$module passed through htmlspecialchars) which will break getByDirname(); revert
to using the raw dirname for the lookup (use $module as received from
Request::getString) when calling $module_handler->getByDirname($module) and only
apply htmlspecialchars at the HTML output point (not before getByDirname),
mirroring the install/uninstall fixes; update references around the case
'update' block (variables $module, htmlspecialchars, getByDirname, $mod)
accordingly.

314-321: ⚠️ Potential issue | 🟠 Major

Remove htmlspecialchars() before filesystem operations — escaping breaks module path lookup.

The dirname is HTML-escaped on line 316 before loadInfoAsVar($module) on line 321. Since basename() does not decode HTML entities, an escaped dirname like foo&bar will be used to construct the filesystem path modules/foo&bar/language/... instead of matching the actual directory foo&bar. This violates the principle of context-appropriate escaping: escape only for output rendering, not for data operations.

Remove the htmlspecialchars() call before the filesystem operation and apply escaping only when rendering the module name in HTML output:

♻️ Proposed fix
    case 'install':
        $module = Request::getString('module', '');
-       $module = htmlspecialchars($module, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        // Get module handler
        /** `@var` XoopsModuleHandler $module_handler */
        $module_handler = xoops_getHandler('module');
        $mod            = $module_handler->create();
        $mod->loadInfoAsVar($module);

Then escape $module only when passing it to xoops_confirm() for HTML rendering, or escape at the point of final output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/main.php` around lines 314 - 321,
The code currently HTML-escapes $module before performing filesystem operations
which breaks module path lookup; remove the htmlspecialchars() call so that the
raw $module value is passed into $module_handler->create() and
$mod->loadInfoAsVar($module) (i.e., stop replacing $module with an escaped
string before loadInfoAsVar), and instead perform escaping only when rendering
to HTML (for example, escape $module when passing it to xoops_confirm() or any
template/output functions). Ensure all references to $module used for filesystem
or handler operations use the unescaped value and only apply htmlspecialchars()
at the output layer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@htdocs/modules/system/admin/modulesadmin/modulesadmin.php`:
- Around line 835-838: The code currently HTML-escapes $dirname with
htmlspecialchars before using it for module lookup, which breaks module
resolution; revert the change so $module_handler->getByDirname() receives the
raw, unescaped $dirname, and only use a new $dirname_escaped (or similar) for
HTML output (logs, image URLs, templates). Locate the htmlspecialchars call and
the call to $module_handler->getByDirname($dirname) and ensure the escape
applies only where rendering occurs, leaving $dirname intact for internal
operations like getByDirname() and subsequent module logic.

In `@tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php`:
- Around line 218-230: The test testUsesHtmlspecialcharsForOutput currently just
checks for the presence of "htmlspecialchars(" but the implementation escapes
dirnames too early (before internal calls like loadInfoAsVar() and
getByDirname()), so fix the implementation to perform htmlspecialchars(...) only
at output boundaries (e.g., where values are assigned into templates or
concatenated into HTML) and not before calling internal handlers; then tighten
the test to assert that htmlspecialchars(...) appears in the template-assignment
or HTML-construction code paths (and does NOT appear immediately surrounding
calls to loadInfoAsVar() or getByDirname()) so the assertion verifies escaping
happens at output, not prior to internal logic.

---

Outside diff comments:
In `@htdocs/modules/system/admin/modulesadmin/main.php`:
- Around line 376-383: The code HTML-escapes $module before calling
$module_handler->getByDirname($module), which breaks the DB lookup; revert so
getByDirname() receives the raw dirname from Request::getString('module', '')
(remove or move the htmlspecialchars call), then apply htmlspecialchars only
when building output ($msgs and in xoops_confirm() calls) so user-facing HTML is
escaped but database lookups use the original $module value.
- Around line 439-445: The update case is escaping the module dirname before the
DB lookup (variable $module passed through htmlspecialchars) which will break
getByDirname(); revert to using the raw dirname for the lookup (use $module as
received from Request::getString) when calling
$module_handler->getByDirname($module) and only apply htmlspecialchars at the
HTML output point (not before getByDirname), mirroring the install/uninstall
fixes; update references around the case 'update' block (variables $module,
htmlspecialchars, getByDirname, $mod) accordingly.
- Around line 314-321: The code currently HTML-escapes $module before performing
filesystem operations which breaks module path lookup; remove the
htmlspecialchars() call so that the raw $module value is passed into
$module_handler->create() and $mod->loadInfoAsVar($module) (i.e., stop replacing
$module with an escaped string before loadInfoAsVar), and instead perform
escaping only when rendering to HTML (for example, escape $module when passing
it to xoops_confirm() or any template/output functions). Ensure all references
to $module used for filesystem or handler operations use the unescaped value and
only apply htmlspecialchars() at the output layer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26ec9a6f-e9d4-4967-91d2-de43655a61ba

📥 Commits

Reviewing files that changed from the base of the PR and between 9eaf7b9 and 043d7b7.

📒 Files selected for processing (3)
  • htdocs/modules/system/admin/modulesadmin/main.php
  • htdocs/modules/system/admin/modulesadmin/modulesadmin.php
  • tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php

…onfig for CodeRabbit

  Address CodeRabbit and Copilot review findings on PR XOOPS#1689, plus fix
  the PHPStan configuration that was blocking CodeRabbit's static analysis.

  Escaping fix:

  The original commit replaced $myts->htmlSpecialChars() with native
  htmlspecialchars() on module dirnames. Both Copilot and CodeRabbit
  flagged that the escaping was applied BEFORE the dirname was used for
  database lookups (getByDirname), filesystem operations (loadInfoAsVar),
  and cache clearing — which would break module resolution if the dirname
  ever contained escapable characters.

  - modulesadmin.php: remove the early htmlspecialchars() on $dirname
    (line 835). The dirname is used raw for DB lookup, cache clear, and
    filesystem include. The only HTML output point ($module->getInfo('author')
    at line 877) was already correctly escaped at the output site.

  - main.php: remove the early htmlspecialchars() on $module in the
    install, uninstall, and update cases (lines 316, 379, 442). These
    values feed directly into loadInfoAsVar() and getByDirname(). The
    xoops_confirm() call handles its own escaping.

  - main.php: keep htmlspecialchars() on $oldname/$newname in the list
    case (lines 268-269) — these are display values going to Smarty
    templates and are correctly escaped at the output point.

  PHPStan configuration:

  - Replace bootstrapFiles (which CodeRabbit disallows for security —
    arbitrary PHP execution) with stubFiles pointing to a new
    phpstan-constants.stub.php that declares XOOPS runtime constants
    as type stubs. The original bootstrapFiles entry referenced a
    non-existent constants.php at the repo root.

  Test update:

  - Strengthen the test to verify both that htmlspecialchars() is present
    (for display escaping) AND that MyTextSanitizer is absent (confirming
    the migration). Previous test was too broad.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
htdocs/modules/system/admin/modulesadmin/main.php (2)

217-217: 🧹 Nitpick | 🔵 Trivial

Same pre-existing inconsistency at line 217.

This htmlspecialchars() call also lacks the explicit 'UTF-8' charset. The changed lines in this PR (268-269) set a good example—consider aligning these in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/main.php` at line 217, The
htmlspecialchars call setting $toinstall_mods[$i]['name'] uses
ENT_QUOTES|ENT_HTML5 but omits the explicit charset; update the call that wraps
$module->getInfo('name') to include 'UTF-8' as the fourth parameter (matching
the changes at the other lines), i.e. locate the assignment to
$toinstall_mods[$i]['name'] and modify the htmlspecialchars invocation to pass
ENT_QUOTES | ENT_HTML5 and 'UTF-8' so encoding is consistent.

138-138: 🧹 Nitpick | 🔵 Trivial

Pre-existing inconsistency: missing charset parameter.

While outside this PR's direct changes, line 138 omits 'UTF-8' whereas the new code at lines 268-269 correctly includes it. Consider a follow-up to normalize these for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/main.php` at line 138, The call that
assigns $listed_mods[$i]['name'] uses htmlspecialchars((string)
$module->getVar('name'), ENT_QUOTES | ENT_HTML5) without the charset argument;
update this to include 'UTF-8' to match other usages (e.g., the code at lines
setting module names later) so replace the htmlspecialchars invocation used in
the $listed_mods assignment to include 'UTF-8' as the third parameter.
htdocs/modules/system/admin/modulesadmin/modulesadmin.php (3)

86-86: 🧹 Nitpick | 🔵 Trivial

Missing 'UTF-8' charset parameter for consistency.

Line 86 omits the charset parameter while line 875 (the changed line) includes 'UTF-8'. For defensive encoding and consistency with the coding guidelines, include the charset explicitly.

-            $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5);
+            $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5, 'UTF-8');

As per coding guidelines: "Escape all output with htmlspecialchars($value, ENT_QUOTES, 'UTF-8')".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/modulesadmin.php` at line 86, Update
the htmlspecialchars call that builds $msgs[] for the module author to include
the explicit charset parameter; specifically change the call wrapping
trim($module->getInfo('author')) in the $msgs[] line so it passes 'UTF-8' as the
third argument (preserving the existing flags ENT_QUOTES | ENT_HTML5) to comply
with the coding guideline.

1500-1500: 🧹 Nitpick | 🔵 Trivial

Missing 'UTF-8' charset parameter in xoops_module_log_header().

-        $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim((string) $module->getInfo('author')), ENT_QUOTES | ENT_HTML5);
+        $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim((string) $module->getInfo('author')), ENT_QUOTES | ENT_HTML5, 'UTF-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/modulesadmin.php` at line 1500, The
xoops_module_log_header() call is missing the explicit 'UTF-8' charset, so
update the invocation to pass 'UTF-8' (matching how you encode module info
displayed here) and ensure the output building line using
htmlspecialchars(trim((string) $module->getInfo('author'))) uses that same
charset context; specifically modify the xoops_module_log_header(...) call to
include 'UTF-8' and keep the htmlspecialchars usage on
$module->getInfo('author') intact so the author string is encoded consistently.

661-661: 🧹 Nitpick | 🔵 Trivial

Missing 'UTF-8' charset parameter—same inconsistency as line 86.

-            $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5);
+            $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5, 'UTF-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/system/admin/modulesadmin/modulesadmin.php` at line 661, The
call to htmlspecialchars in the modules list is missing the explicit 'UTF-8'
encoding parameter (in the expression building $msgs[] with
trim($module->getInfo('author')) and ENT_QUOTES | ENT_HTML5); update that
htmlspecialchars invocation to pass 'UTF-8' as the third argument to match the
encoding used elsewhere (same fix as applied at line 86) so character encoding
is explicit and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@phpstan.neon`:
- Around line 22-23: The phpstan.neon references a non-existent stub file under
the stubFiles key: phpstan-constants.stub.php; either create that stub file with
the required constant/type definitions or remove the entry from phpstan.neon
(edit the stubFiles list) so PHPStan will not try to load a missing file. Ensure
the change targets the phpstan.neon configuration and the
phpstan-constants.stub.php identifier so future static analysis runs succeed.

---

Outside diff comments:
In `@htdocs/modules/system/admin/modulesadmin/main.php`:
- Line 217: The htmlspecialchars call setting $toinstall_mods[$i]['name'] uses
ENT_QUOTES|ENT_HTML5 but omits the explicit charset; update the call that wraps
$module->getInfo('name') to include 'UTF-8' as the fourth parameter (matching
the changes at the other lines), i.e. locate the assignment to
$toinstall_mods[$i]['name'] and modify the htmlspecialchars invocation to pass
ENT_QUOTES | ENT_HTML5 and 'UTF-8' so encoding is consistent.
- Line 138: The call that assigns $listed_mods[$i]['name'] uses
htmlspecialchars((string) $module->getVar('name'), ENT_QUOTES | ENT_HTML5)
without the charset argument; update this to include 'UTF-8' to match other
usages (e.g., the code at lines setting module names later) so replace the
htmlspecialchars invocation used in the $listed_mods assignment to include
'UTF-8' as the third parameter.

In `@htdocs/modules/system/admin/modulesadmin/modulesadmin.php`:
- Line 86: Update the htmlspecialchars call that builds $msgs[] for the module
author to include the explicit charset parameter; specifically change the call
wrapping trim($module->getInfo('author')) in the $msgs[] line so it passes
'UTF-8' as the third argument (preserving the existing flags ENT_QUOTES |
ENT_HTML5) to comply with the coding guideline.
- Line 1500: The xoops_module_log_header() call is missing the explicit 'UTF-8'
charset, so update the invocation to pass 'UTF-8' (matching how you encode
module info displayed here) and ensure the output building line using
htmlspecialchars(trim((string) $module->getInfo('author'))) uses that same
charset context; specifically modify the xoops_module_log_header(...) call to
include 'UTF-8' and keep the htmlspecialchars usage on
$module->getInfo('author') intact so the author string is encoded consistently.
- Line 661: The call to htmlspecialchars in the modules list is missing the
explicit 'UTF-8' encoding parameter (in the expression building $msgs[] with
trim($module->getInfo('author')) and ENT_QUOTES | ENT_HTML5); update that
htmlspecialchars invocation to pass 'UTF-8' as the third argument to match the
encoding used elsewhere (same fix as applied at line 86) so character encoding
is explicit and consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86549213-9e09-4260-aaed-39d9070241b3

📥 Commits

Reviewing files that changed from the base of the PR and between 043d7b7 and 075b5e6.

📒 Files selected for processing (4)
  • htdocs/modules/system/admin/modulesadmin/main.php
  • htdocs/modules/system/admin/modulesadmin/modulesadmin.php
  • phpstan.neon
  • tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

htdocs/modules/system/admin/modulesadmin/main.php:321

  • In the install flow, $module comes from Request::getString('module', '') and is passed directly to $mod->loadInfoAsVar($module) without validating that the module directory exists / that loadInfoAsVar() succeeded. If an invalid module value is provided, loadInfo() can fail and subsequent $mod->getInfo()/getVar() calls may error. Consider validating $module against XoopsLists::getModulesList() (or checking the loadInfoAsVar()/loadInfo() return) and redirecting on failure.
    case 'install':
        $module = Request::getString('module', '');
        // Get module handler
        /** @var XoopsModuleHandler $module_handler */
        $module_handler = xoops_getHandler('module');
        $mod            = $module_handler->create();
        $mod->loadInfoAsVar($module);
        // Construct message

htdocs/modules/system/admin/modulesadmin/main.php:382

  • In the uninstall flow, $mod = $module_handler->getByDirname($module) can return false (per handler contract), but the code immediately calls $mod->getInfo()/$mod->getVar(). Add an instanceof XoopsModule (or is_object) check and handle the error (e.g., redirect) to avoid fatal errors on invalid module input.
    case 'uninstall':
        $module = Request::getString('module', '');
        $msgs = '';
        // Get module handler
        /** @var XoopsModuleHandler $module_handler */
        $module_handler = xoops_getHandler('module');
        $mod            = $module_handler->getByDirname($module);
        // Construct message

htdocs/modules/system/admin/modulesadmin/main.php:445

  • In the update flow, $mod = $module_handler->getByDirname($module) can return false, but the code assumes it is an object and calls $mod->getInfo()/getVar(). Add a guard (e.g., if (!$mod instanceof XoopsModule) redirect_header(...)) to prevent a fatal error when an invalid module is provided.
    case 'update':
        $module = Request::getString('module', '');
        // Get module handler
        /** @var XoopsModuleHandler $module_handler */
        $module_handler = xoops_getHandler('module');
        $mod            = $module_handler->getByDirname($module);
        // Construct message
        if ($mod->getInfo('image') !== false && trim($mod->getInfo('image')) != '') {
            $msgs = '<img src="' . XOOPS_URL . '/modules/' . $mod->getVar('dirname', 'n') . '/' . trim($mod->getInfo('image')) . '" alt="" />';

Comment on lines +22 to +23
stubFiles:
- phpstan-constants.stub.php
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phpstan.neon now references phpstan-constants.stub.php under stubFiles, but that file does not exist in the repository (and constants.php is also not present at repo root). This will break PHPStan runs unless the stub file is added (or the path updated to where it actually lives).

Suggested change
stubFiles:
- phpstan-constants.stub.php

Copilot uses AI. Check for mistakes.
Comment on lines 20 to +23
scanFiles:
- htdocs/xoops_lib/vendor/kint-php/kint/src/Kint.php
bootstrapFiles:
- constants.php
stubFiles:
- phpstan-constants.stub.php
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description’s explicit file list does not mention phpstan.neon, but this PR modifies it. Either update the PR description to include this change or move the PHPStan config update into a separate PR to keep the scope clear.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines 217 to 242
/**
* Verify that MyTextSanitizer is used for HTML output sanitization.
* Verify that htmlspecialchars is used for display output, but NOT for
* dirnames that feed into DB lookups and filesystem operations.
*/
public function testUsesTextSanitizerForOutput(): void
public function testUsesHtmlspecialcharsForDisplayOnly(): void
{
// Display names (list operation) are escaped for HTML output
$this->assertStringContainsString(
'$myts = \MyTextSanitizer::getInstance();',
'htmlspecialchars(',
$this->sourceCode,
'Should initialize MyTextSanitizer'
'Should use htmlspecialchars for display output'
);

// Should use htmlSpecialChars for output
$this->assertStringContainsString(
// MyTextSanitizer should no longer be used — dirnames and display
// names now use native htmlspecialchars where appropriate
$this->assertStringNotContainsString(
'$myts->htmlSpecialChars',
$this->sourceCode,
'Should use htmlSpecialChars for output sanitization'
'Should not use MyTextSanitizer htmlSpecialChars'
);
$this->assertStringNotContainsString(
'MyTextSanitizer::getInstance()',
$this->sourceCode,
'Should not instantiate MyTextSanitizer'
);
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docblock and method name claim this test verifies that htmlspecialchars is used for display output but not for dirnames used in DB/filesystem operations, but the assertions only check that htmlspecialchars( appears somewhere and that MyTextSanitizer usage is absent. Either tighten the assertions to specifically cover the dirname/non-dirname behavior described, or adjust the comment/name so they match what the test actually verifies.

Copilot uses AI. Check for mistakes.
…ions

  Address CodeRabbit and Copilot review findings on the second push:

  - Add phpstan-constants.stub.php that was referenced by phpstan.neon
    but never committed. Both CodeRabbit and Copilot flagged this as
    critical — PHPStan runs would fail with a missing file error.

  - Tighten test assertions per Copilot feedback:
    - testUsesHtmlspecialcharsForDisplayOnly now asserts the specific
      ENT_QUOTES | ENT_HTML5 flags are present, not just the function name.
    - New testDirnamesNotEscapedBeforeDbLookup verifies that the install,
      uninstall, and update operations do NOT escape $module before passing
      it to loadInfoAsVar() / getByDirname(). This directly tests the
      escape-at-output-not-input fix from the previous commit.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@phpstan-constants.stub.php`:
- Around line 1-11: Add the standard XOOPS copyright header block at the top of
phpstan-constants.stub.php so the file begins with the required XOOPS header
before the existing docblock; specifically, prepend the standard multi-line
XOOPS copyright header immediately above the current "/** PHPStan constant stubs
for XOOPS core." docblock (i.e., before the existing PHP file-level comment) so
the file starts with the mandated header.

In `@tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php`:
- Around line 246-273: The test testDirnamesNotEscapedBeforeDbLookup currently
only asserts dirname non-escaping for 'install', 'uninstall', and 'update'
sections; extend it to also cover the '_ok' branches so regressions in the
success-handling paths are caught. Use the same pattern calling
extractOperationSection('install_ok'), extractOperationSection('uninstall_ok'),
and extractOperationSection('update_ok'), assert each is not empty with
assertNotEmpty, and run the same
assertStringNotContainsString('htmlspecialchars($module', ...) checks (same
message text) for each *_ok section to mirror the existing checks.
- Around line 221-227: The test testUsesHtmlspecialcharsForDisplayOnly currently
only checks for the flag substring in $this->sourceCode; change the assertion to
verify the full call shape including flags and charset by asserting that
htmlspecialchars is invoked as htmlspecialchars(..., ENT_QUOTES | ENT_HTML5,
'UTF-8') (or use an appropriate regex/assertMatchesRegularExpression on
$this->sourceCode) so the test confirms both the flag combination and the
'UTF-8' charset are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3d21d68-9976-40ba-a9ef-fee3e89bab8e

📥 Commits

Reviewing files that changed from the base of the PR and between 075b5e6 and b17dd2e.

📒 Files selected for processing (2)
  • phpstan-constants.stub.php
  • tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +9 to +10
*
* @phpstan-type non-empty-string
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PHPDoc tag @phpstan-type non-empty-string is syntactically invalid because @phpstan-type requires an alias (e.g. @phpstan-type NonEmptyString non-empty-string) and here it has none. This can cause PHPStan to error while parsing stubs; either remove this line or rewrite it with a proper alias and then actually use that alias in annotations.

Suggested change
*
* @phpstan-type non-empty-string

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
define('XOOPS_GROUP_ADMIN', 1);
define('XOOPS_GROUP_USERS', 2);
define('XOOPS_GROUP_ANONYMOUS', 3);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These stubbed group constants don’t match the types in htdocs/mainfile.dist.php (there they are defined as strings like '1', '2', '3'). Using ints here can change PHPStan’s inferred types and make analysis results diverge from runtime; align the stub values/types with the real definitions.

Suggested change
define('XOOPS_GROUP_ADMIN', 1);
define('XOOPS_GROUP_USERS', 2);
define('XOOPS_GROUP_ANONYMOUS', 3);
define('XOOPS_GROUP_ADMIN', '1');
define('XOOPS_GROUP_USERS', '2');
define('XOOPS_GROUP_ANONYMOUS', '3');

Copilot uses AI. Check for mistakes.
…r _ok branches

  Address all CodeRabbit and Copilot findings from the latest review round
  in one comprehensive pass:

  - phpstan-constants.stub.php: add required XOOPS copyright header block,
    remove invalid @phpstan-type annotation (missing alias), change group
    constants from int to string ('1','2','3') matching mainfile.dist.php.

  - MainControllerTest: strengthen testUsesHtmlspecialcharsForDisplayOnly
    to assert the full call shape including 'UTF-8' charset, not just the
    flag constants. Add assertions for install_ok, uninstall_ok, and
    update_ok operation branches in testDirnamesNotEscapedBeforeDbLookup
    to cover the actual execution paths alongside the confirmation dialogs.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php (1)

1218-1220: 🧹 Nitpick | 🔵 Trivial

Minor dead code: $endPos === false check is unreachable.

At line 1218, $endPos is always assigned a value by lines 1209-1215. If $nextCase is false, the else branch ensures $endPos gets either $lastBreak + 6 or strlen($this->sourceCode). The condition can never be true.

♻️ Optional cleanup
-        if ($endPos === false) {
-            return '';
-        }
-
         return substr($this->sourceCode, $startPos, $endPos - $startPos);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php`
around lines 1218 - 1220, The check `if ($endPos === false) return '';` is
unreachable because `$endPos` is always set by the preceding `if ($nextCase !==
false) { ... } else { $endPos = $lastBreak + 6; /* or */
strlen($this->sourceCode); }` logic; remove this dead branch. Edit the test in
MainControllerTest to delete the unreachable `$endPos === false` check (or
replace it with an assertion/comment if you prefer) and ensure references to
`$endPos`, `$nextCase`, `$lastBreak`, and `$this->sourceCode` remain unchanged
so the remaining flow still uses the computed `$endPos`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php`:
- Around line 1218-1220: The check `if ($endPos === false) return '';` is
unreachable because `$endPos` is always set by the preceding `if ($nextCase !==
false) { ... } else { $endPos = $lastBreak + 6; /* or */
strlen($this->sourceCode); }` logic; remove this dead branch. Edit the test in
MainControllerTest to delete the unreachable `$endPos === false` check (or
replace it with an assertion/comment if you prefer) and ensure references to
`$endPos`, `$nextCase`, `$lastBreak`, and `$this->sourceCode` remain unchanged
so the remaining flow still uses the computed `$endPos`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed86a198-9a2f-4cf2-b271-c6ebb5f88e1e

📥 Commits

Reviewing files that changed from the base of the PR and between b17dd2e and 63e8ba9.

📒 Files selected for processing (2)
  • phpstan-constants.stub.php
  • tests/unit/htdocs/modules/system/admin/modulesadmin/MainControllerTest.php

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +871 to +875
@@ -876,7 +872,7 @@ function xoops_module_update($dirname)
}
$msgs[] = '<strong>' . _VERSION . ':</strong> ' . $module->getInfo('version');
if ($module->getInfo('author') !== false && trim($module->getInfo('author')) != '') {
$msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . $myts->htmlSpecialChars(trim($module->getInfo('author')));
$msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5, 'UTF-8');
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In xoops_module_update(), $dirname is no longer HTML-escaped (the prior MyTextSanitizer call was removed), but it’s still interpolated into an HTML attribute for the module image URL. If a module dirname contains characters like quotes or <, this becomes an XSS risk in the admin UI. Keep $dirname unmodified for handler/path operations, but introduce a separately escaped/URL-encoded value when embedding it into HTML/URLs (and use that for the img src path segment).

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +224
// Display names in the list/confirm operation must use the full
// htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8') call shape
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline test comment says the list/confirm display output “must use” the full htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8') call shape, but the source under test still has list/installlist escaping calls without the explicit charset. Consider tightening the wording to match what the test actually enforces (presence of at least one call using the explicit UTF-8 form) to avoid misleading future maintainers.

Suggested change
// Display names in the list/confirm operation must use the full
// htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8') call shape
// Verify the source contains at least one htmlspecialchars call using
// ENT_QUOTES | ENT_HTML5 with an explicit 'UTF-8' charset.

Copilot uses AI. Check for mistakes.
  The earlier commit removed early htmlspecialchars() on $dirname to fix
  the DB lookup bug, but left the img src output point unescaped. If a
  module dirname or image filename contained quotes or angle brackets,
  the raw interpolation into the src attribute would be an XSS vector.

  Escape both $dirname and $module->getInfo('image') at the two HTML
  output sites (lines 657 and 871 in xoops_module_install and
  xoops_module_update) using htmlspecialchars() with ENT_QUOTES |
  ENT_HTML5. The dirname stays raw for all internal operations (DB
  lookup, filesystem, cache clearing).
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 7, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

htdocs/modules/system/admin/modulesadmin/modulesadmin.php:661

  • In this log header, $dirname/image are escaped with htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8'), but the author line uses htmlspecialchars(..., ENT_QUOTES | ENT_HTML5) without an explicit charset. For consistent output encoding (and to avoid relying on default_charset), pass the same 'UTF-8' (or _CHARSET) third argument here as well.
            $msgs[] = '<img src="' . XOOPS_URL . '/modules/' . htmlspecialchars($dirname, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '/' . htmlspecialchars(trim($module->getInfo('image')), ENT_QUOTES | ENT_HTML5, 'UTF-8') . '" alt="" />';
        }
        $msgs[] = '<strong>' . _VERSION . ':</strong> ' . $module->getInfo('version');
        if ($module->getInfo('author') !== false && trim($module->getInfo('author')) != '') {
            $msgs[] = '<strong>' . _AUTHOR . ':</strong> ' . htmlspecialchars(trim($module->getInfo('author')), ENT_QUOTES | ENT_HTML5);

Comment on lines +250 to +299
// used for loadInfoAsVar() and getByDirname(). It must NOT be escaped
// before those calls — escaping would change the identifier.
$installSection = $this->extractOperationSection('install');
$this->assertNotEmpty($installSection, 'Install operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$installSection,
'Install operation must not escape $module dirname before DB lookup'
);

$uninstallSection = $this->extractOperationSection('uninstall');
$this->assertNotEmpty($uninstallSection, 'Uninstall operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$uninstallSection,
'Uninstall operation must not escape $module dirname before DB lookup'
);

$updateSection = $this->extractOperationSection('update');
$this->assertNotEmpty($updateSection, 'Update operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$updateSection,
'Update operation must not escape $module dirname before DB lookup'
);

// The _ok branches perform the actual module install/uninstall/update
// and also use $module as a dirname for handler lookups.
$installOkSection = $this->extractOperationSection('install_ok');
$this->assertNotEmpty($installOkSection, 'install_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$installOkSection,
'install_ok operation must not escape $module dirname'
);

$uninstallOkSection = $this->extractOperationSection('uninstall_ok');
$this->assertNotEmpty($uninstallOkSection, 'uninstall_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$uninstallOkSection,
'uninstall_ok operation must not escape $module dirname'
);

$updateOkSection = $this->extractOperationSection('update_ok');
$this->assertNotEmpty($updateOkSection, 'update_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$updateOkSection,
'update_ok operation must not escape $module dirname'
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions check that the operation sections do not contain the substring htmlspecialchars($module anywhere, but the docstring says “must NOT be escaped before DB lookup”. As written, the test would also fail if $module were escaped later for display (which would be valid) and doesn’t actually assert what argument is passed to loadInfoAsVar() / getByDirname(). Consider asserting directly that the lookup calls use raw $module (e.g., match loadInfoAsVar($module) / getByDirname($module) and ensure those call expressions do not wrap $module), instead of banning the substring in the whole section.

Suggested change
// used for loadInfoAsVar() and getByDirname(). It must NOT be escaped
// before those calls — escaping would change the identifier.
$installSection = $this->extractOperationSection('install');
$this->assertNotEmpty($installSection, 'Install operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$installSection,
'Install operation must not escape $module dirname before DB lookup'
);
$uninstallSection = $this->extractOperationSection('uninstall');
$this->assertNotEmpty($uninstallSection, 'Uninstall operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$uninstallSection,
'Uninstall operation must not escape $module dirname before DB lookup'
);
$updateSection = $this->extractOperationSection('update');
$this->assertNotEmpty($updateSection, 'Update operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$updateSection,
'Update operation must not escape $module dirname before DB lookup'
);
// The _ok branches perform the actual module install/uninstall/update
// and also use $module as a dirname for handler lookups.
$installOkSection = $this->extractOperationSection('install_ok');
$this->assertNotEmpty($installOkSection, 'install_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$installOkSection,
'install_ok operation must not escape $module dirname'
);
$uninstallOkSection = $this->extractOperationSection('uninstall_ok');
$this->assertNotEmpty($uninstallOkSection, 'uninstall_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$uninstallOkSection,
'uninstall_ok operation must not escape $module dirname'
);
$updateOkSection = $this->extractOperationSection('update_ok');
$this->assertNotEmpty($updateOkSection, 'update_ok operation should exist');
$this->assertStringNotContainsString(
'htmlspecialchars($module',
$updateOkSection,
'update_ok operation must not escape $module dirname'
// used for loadInfoAsVar() and getByDirname(). It must be passed raw
// to those lookup calls — escaping would change the identifier.
$installSection = $this->extractOperationSection('install');
$this->assertNotEmpty($installSection, 'Install operation should exist');
$this->assertMatchesRegularExpression(
'/loadInfoAsVar\(\s*\$module\s*\)/',
$installSection,
'Install operation should pass raw $module to loadInfoAsVar()'
);
$uninstallSection = $this->extractOperationSection('uninstall');
$this->assertNotEmpty($uninstallSection, 'Uninstall operation should exist');
$this->assertMatchesRegularExpression(
'/getByDirname\(\s*\$module\s*\)/',
$uninstallSection,
'Uninstall operation should pass raw $module to getByDirname()'
);
$updateSection = $this->extractOperationSection('update');
$this->assertNotEmpty($updateSection, 'Update operation should exist');
$this->assertMatchesRegularExpression(
'/loadInfoAsVar\(\s*\$module\s*\)/',
$updateSection,
'Update operation should pass raw $module to loadInfoAsVar()'
);
// The _ok branches perform the actual module install/uninstall/update
// and also use $module as a dirname for handler lookups.
$installOkSection = $this->extractOperationSection('install_ok');
$this->assertNotEmpty($installOkSection, 'install_ok operation should exist');
$this->assertMatchesRegularExpression(
'/getByDirname\(\s*\$module\s*\)/',
$installOkSection,
'install_ok operation should pass raw $module to getByDirname()'
);
$uninstallOkSection = $this->extractOperationSection('uninstall_ok');
$this->assertNotEmpty($uninstallOkSection, 'uninstall_ok operation should exist');
$this->assertMatchesRegularExpression(
'/getByDirname\(\s*\$module\s*\)/',
$uninstallOkSection,
'uninstall_ok operation should pass raw $module to getByDirname()'
);
$updateOkSection = $this->extractOperationSection('update_ok');
$this->assertNotEmpty($updateOkSection, 'update_ok operation should exist');
$this->assertMatchesRegularExpression(
'/getByDirname\(\s*\$module\s*\)/',
$updateOkSection,
'update_ok operation should pass raw $module to getByDirname()'

Copilot uses AI. Check for mistakes.
@mambax7 mambax7 merged commit 681665f into XOOPS:master Apr 7, 2026
16 of 17 checks passed
@mambax7 mambax7 deleted the fix/issue-869-htmlspecialchars-modulesadmin branch April 7, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$myts->htmlSpecialChars() with htmlspecialchars()

2 participants