Skip to content

Pin all Request calls to explicit GET/POST hash, fix elvis on zero-valid fields#1650

Merged
mambax7 merged 3 commits into
XOOPS:masterfrom
mambax7:fix/rc-request-hash-pinning
Mar 14, 2026
Merged

Pin all Request calls to explicit GET/POST hash, fix elvis on zero-valid fields#1650
mambax7 merged 3 commits into
XOOPS:masterfrom
mambax7:fix/rc-request-hash-pinning

Conversation

@mambax7

@mambax7 mambax7 commented Mar 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • H-1: Pin ~30 hashless Request::getXxx() calls to explicit 'GET' or 'POST' across 8 files, using dual-source hasVar pattern for op parameters that can come from either method
  • H-2: Replace all ?: elvis operators on zero-valid fields in profile/search.php with hasVar ternary to prevent 0 being treated as falsy

Files modified

  • system/admin/groups/main.php, comments/main.php, preferences/main.php, smilies/main.php, maintenance/main.php, userrank/main.php
  • profile/admin/category.php, profile/search.php

Test plan

  • 34 tests scanning all 17 listed files for hashless Request calls and elvis patterns
  • Manual test: admin pages with GET parameters still work (op=edit, pagination)
  • Manual test: form submissions still process correctly via POST
  • Manual test: profile search with 0 values for match type and pagination offset

Summary by CodeRabbit

  • Bug Fixes

    • Improved request input handling to prioritize secure HTTP methods and enforce stricter validation across admin modules.
    • Enhanced form parameter processing with explicit method enforcement for better data consistency.
  • Tests

    • Added comprehensive test suite for request security validation and hardening.

Replace ~30 hashless Request::getXxx('key', default) calls with
explicit 'GET' or 'POST' hash arguments across system admin pages
(groups, comments, preferences, smilies, maintenance, userrank),
profile admin (category), and profile search.

Key changes:
- All op parameters use dual-source hasVar pattern for GET/POST
- Pagination params (start, limit) pinned to 'GET'
- Form field values pinned to 'POST'
- Display/filter params pinned to 'GET'
- Replace elvis (?:) dual-source patterns in profile/search.php
  with hasVar ternary to avoid zero-valid field bugs (_larger,
  _smaller, _match, start, limit, fieldValues)
- Fix category.php delete case: id=0 with ?: would skip POST

Add PHPUnit source-scanning test that verifies no hashless
Request::getXxx() calls remain and no elvis operator is used
for dual-source Request patterns in all hardened files.
Copilot AI review requested due to automatic review settings March 12, 2026 20:59
@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown

Walkthrough

These changes systematically refactor input source handling across multiple admin modules to enforce explicit HTTP method preferences—favoring POST for action parameters and GET for pagination. Additionally, a new test suite validates that all Request::getXxx() calls include explicit hash arguments as the third parameter.

Changes

Cohort / File(s) Summary
Input Source Hardening
htdocs/modules/profile/admin/category.php, htdocs/modules/system/admin/groups/main.php, htdocs/modules/system/admin/comments/main.php, htdocs/modules/system/admin/preferences/main.php, htdocs/modules/system/admin/maintenance/main.php, htdocs/modules/system/admin/smilies/main.php, htdocs/modules/system/admin/userrank/main.php
Systematically shift input retrieval to prefer POST for action parameters (op) and enforce GET-only for pagination (start). Operation parameters now check POST presence first before defaulting to GET, and several modules restrict specific inputs to POST-only retrieval for save operations.
Profile Search Logic
htdocs/modules/profile/search.php
Enhanced input validation with hasVar checks to conditionally prefer GET over POST for form fields (uname, email, numeric ranges, field-based values). Added regex validation for uname with early redirect on invalid input; introduces LIKE-based SQL search with prepared statements and processes results into search metadata.
Hash Pinning Test Suite
tests/unit/htdocs/hardening/RequestHashPinningTest.php
New unit test class validating two hash-pinning constraints: all Request::getXxx() calls must include an explicit hash argument as the third parameter, and no dual-source Elvis operator patterns. Implements lightweight argument parsing with balanced-parentheses extraction and per-line violation detection across predefined hardened file paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main changes: pinning Request calls to explicit GET/POST hashes and fixing elvis operator usage on zero-valid fields, which are the primary objectives of this PR.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Hardens request-variable handling by making Xmf\Request::getXxx() call sites explicit about the source hash (GET vs POST), and removes ?: elvis patterns where 0/empty-string values must be preserved.

Changes:

  • Pin Request::getXxx() calls to explicit 'GET'/'POST' in multiple system admin controllers and profile modules.
  • Replace elvis-based dual-source request reads in profile/search.php with hasVar()-based selection to preserve zero-valid values.
  • Add a unit test to enforce “no hashless Request calls” and “no elvis dual-source Request calls” across the hardened file set.

Reviewed changes

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

Show a summary per file
File Description
tests/unit/htdocs/hardening/RequestHashPinningTest.php Adds static-analysis tests to detect hashless Request::getXxx() calls and elvis dual-source patterns.
htdocs/modules/system/admin/userrank/main.php Pins op, paging, and form fields to explicit GET/POST sources.
htdocs/modules/system/admin/smilies/main.php Pins op, paging, and form fields to explicit GET/POST sources.
htdocs/modules/system/admin/preferences/main.php Pins op and confcat_id to explicit sources.
htdocs/modules/system/admin/maintenance/main.php Pins op and maintenance form inputs to explicit POST.
htdocs/modules/system/admin/groups/main.php Pins op to explicit GET/POST source selection.
htdocs/modules/system/admin/comments/main.php Pins op, com_id, and pagination inputs to explicit sources.
htdocs/modules/profile/search.php Replaces elvis dual-source reads with hasVar() selection for multiple search inputs.
htdocs/modules/profile/admin/category.php Replaces elvis dual-source id selection with hasVar() ternary.

$criteria->setOrder($order);

$limit = Request::getInt('limit', 0, 'GET') ?: Request::getInt('limit', $limit_default, 'POST');
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

$limit can now be 0 (or negative) when provided via GET because the code checks hasVar() rather than using an elvis/default fallback. In ProfileProfileHandler::search() a criteria limit of 0 means “no LIMIT”, so this can trigger an unbounded user query (potentially very expensive). Clamp/validate limit to a sane range (e.g., >=1 and <= max) before calling setLimit(), or treat 0 as “use default”.

Suggested change
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
if ($limit < 1) {
$limit = $limit_default;
}

Copilot uses AI. Check for mistakes.
self::markTestSkipped("File not found: {$filePath}");
}

$source = file_get_contents($filePath);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

file_get_contents($filePath) can return false even when file_exists() is true (permissions, transient FS errors). With strict_types=1, passing false into explode() will raise a TypeError and turn this into a fatal error instead of a clean test failure. Add an assertNotFalse($source) (and optionally assertNotEmpty) after reading the file, similar to how other source-analysis tests handle this.

Suggested change
$source = file_get_contents($filePath);
$source = file_get_contents($filePath);
self::assertNotFalse(
$source,
sprintf('Unable to read source file: %s', basename($filePath))
);
self::assertNotEmpty(
$source,
sprintf('Source file is empty: %s', basename($filePath))
);

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +116
$source = file_get_contents($filePath);
$lines = explode("\n", $source);

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

Same as above: file_get_contents($filePath) isn’t checked for false before being used, which can cause a TypeError and abort the test run. Add an assertNotFalse($source) (and optionally assertNotEmpty) after reading the file so failures report cleanly.

Copilot uses AI. Check for mistakes.
mambax7 added 2 commits March 13, 2026 12:32
  Fixes B-1 showstopper on PR XOOPS#1650. The smilies_update_display and userrank_update_special toggle handlers read IDs from GET only, but system_setStatus()
   in admin.js sends them via $.post(). Changed both to Request::getInt(..., 0, 'POST') so the AJAX on/off toggles work correctly.
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
htdocs/modules/profile/search.php (1)

302-340: ⚠️ Potential issue | 🟡 Minor

Zero-valued range bounds are still silently dropped.

The guards at Line 311, Line 321, Line 331, and Line 337 still treat 0 as “not provided”, so the new hasVar() reads at Line 302 and Line 303 do not actually make _larger=0 / _smaller=0 searchable. Use presence/validation checks instead of numeric truthiness here.

🧩 One way to preserve 0 without accepting invalid input
-                                $intLarger  = Request::hasVar($fieldname . '_larger', 'GET') ? Request::getInt($fieldname . '_larger', 0, 'GET') : Request::getInt($fieldname . '_larger', 0, 'POST');
-                                $intSmaller = Request::hasVar($fieldname . '_smaller', 'GET') ? Request::getInt($fieldname . '_smaller', 0, 'GET') : Request::getInt($fieldname . '_smaller', 0, 'POST');
-                                if ($intLarger !== 0) {
+                                if ($largerVal !== '' && filter_var($largerVal, FILTER_VALIDATE_INT) !== false) {
+                                    $intLarger = (int) $largerVal;
                                     $search_url[] = $fieldname . '_larger=' . $intLarger;
                                     $searchvars[] = $fieldname;
                                     $criteria->add(new Criteria($fieldname, $intLarger, '>='));
                                 }
 
-                                if ($intSmaller !== 0) {
+                                if ($smallerVal !== '' && filter_var($smallerVal, FILTER_VALIDATE_INT) !== false) {
+                                    $intSmaller = (int) $smallerVal;
                                     $search_url[] = $fieldname . '_smaller=' . $intSmaller;
                                     $searchvars[] = $fieldname;
                                     $criteria->add(new Criteria($fieldname, $intSmaller, '<='));
                                 }

Apply the same presence-based check in the date / datetime branch so epoch 0 is not discarded there either.

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

In `@htdocs/modules/profile/search.php` around lines 302 - 340, The code currently
drops zero bounds by testing numeric truthiness; update the date/datetime and
default branches to use presence and explicit validation instead of >0/!==0
checks: for $largerVal/$smallerVal call strtotime and treat the result as valid
when !== false (or cast to int and allow 0) and use
Request::hasVar($fieldname.'_larger'...) /
Request::hasVar($fieldname.'_smaller'...) to decide whether to add a criteria;
for the integer branch use Request::hasVar to detect presence and validate with
is_numeric (or Request::getInt but gated by hasVar) before adding
$intLarger/$intSmaller to $search_url, $searchvars and $criteria so a provided 0
is preserved while still rejecting non-numeric input.
🤖 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/profile/search.php`:
- Around line 439-442: The code currently only enforces a lower bound for $limit
but allows arbitrarily large values to reach $criteria->setLimit(), so add an
upper clamp after reading the value: compute $limit using Request::getInt as
before, then if $limit < 1 set to $limit_default and if $limit > <CAP> set to
<CAP> (use an existing configuration constant or a module-level MAX_LIMIT
instead of a literal); update any relevant documentation/comments and ensure
$criteria->setLimit($limit) receives the clamped value.

In `@tests/unit/htdocs/hardening/RequestHashPinningTest.php`:
- Around line 121-122: The regex stored in $elvisPattern uses [^)]* which fails
on nested parentheses in argument lists (so Request::getInt(... max(0, $x) ...)
will not match); replace the naive [^)]* with a more robust pattern that allows
nested parentheses or a balanced group (e.g., use a non-greedy match with
optional nested-parentheses handling or a recursive pattern if PCRE is enabled)
so the pattern still targets the Request::getString|getInt|... call followed by
?: Request::; update the $elvisPattern variable accordingly to ensure it matches
calls with complex arguments.
- Around line 54-106: The noHashlessRequestCalls method is over-complex; extract
the inner match-handling logic into a private helper to reduce nesting and
cognitive complexity. Create a helper (e.g., handleRequestCallMatch or
collectHashlessRequestCall) that accepts the original line, the match full text
and offset, the line number and a reference to $violations; move the parenPos
calculation, call to extractBalancedArgs, argCount via countArguments, and the
violation push (when argCount <= 2) into that helper. Replace the inner loop
body in noHashlessRequestCalls with a call to this helper for each match so the
main test reads as a simple loop over matches and delegates parsing/violation
logic to the new method; continue using CALL_START_PATTERN, extractBalancedArgs
and countArguments as-is.
- Around line 7-9: The tests use PHPUnit 10 attributes which break compatibility
with PHPUnit 9.6; update the two test methods noHashlessRequestCalls and
noElvisOperatorOnDualSourceRequestCalls to use docblock annotations (`@test` and
`@dataProvider`) instead of #[Test] and #[DataProvider], remove the now-unused use
statements for PHPUnit\Framework\Attributes\Test and
PHPUnit\Framework\Attributes\DataProvider, and ensure the data provider names
referenced in the docblocks match the existing provider methods.

---

Outside diff comments:
In `@htdocs/modules/profile/search.php`:
- Around line 302-340: The code currently drops zero bounds by testing numeric
truthiness; update the date/datetime and default branches to use presence and
explicit validation instead of >0/!==0 checks: for $largerVal/$smallerVal call
strtotime and treat the result as valid when !== false (or cast to int and allow
0) and use Request::hasVar($fieldname.'_larger'...) /
Request::hasVar($fieldname.'_smaller'...) to decide whether to add a criteria;
for the integer branch use Request::hasVar to detect presence and validate with
is_numeric (or Request::getInt but gated by hasVar) before adding
$intLarger/$intSmaller to $search_url, $searchvars and $criteria so a provided 0
is preserved while still rejecting non-numeric input.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 63cf4713-23d5-4396-b77a-583b77afa746

📥 Commits

Reviewing files that changed from the base of the PR and between cda69e2 and 2b4e4f5.

📒 Files selected for processing (9)
  • htdocs/modules/profile/admin/category.php
  • htdocs/modules/profile/search.php
  • htdocs/modules/system/admin/comments/main.php
  • htdocs/modules/system/admin/groups/main.php
  • htdocs/modules/system/admin/maintenance/main.php
  • htdocs/modules/system/admin/preferences/main.php
  • htdocs/modules/system/admin/smilies/main.php
  • htdocs/modules/system/admin/userrank/main.php
  • tests/unit/htdocs/hardening/RequestHashPinningTest.php

Comment on lines +439 to +442
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
if ($limit < 1) {
$limit = $limit_default;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clamp limit on the upper end too.

This guards against < 1, but ?limit=1000000 still flows straight into $criteria->setLimit(). On a public search endpoint, that can turn a cheap paged query into a very large read and render.

📏 Suggested cap
         $limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
         if ($limit < 1) {
             $limit = $limit_default;
+        } elseif ($limit > 100) {
+            $limit = 100;
         }

If the cap is configurable elsewhere, use that constant/config value instead of a literal.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
if ($limit < 1) {
$limit = $limit_default;
}
$limit = Request::hasVar('limit', 'GET') ? Request::getInt('limit', $limit_default, 'GET') : Request::getInt('limit', $limit_default, 'POST');
if ($limit < 1) {
$limit = $limit_default;
} elseif ($limit > 100) {
$limit = 100;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@htdocs/modules/profile/search.php` around lines 439 - 442, The code currently
only enforces a lower bound for $limit but allows arbitrarily large values to
reach $criteria->setLimit(), so add an upper clamp after reading the value:
compute $limit using Request::getInt as before, then if $limit < 1 set to
$limit_default and if $limit > <CAP> set to <CAP> (use an existing configuration
constant or a module-level MAX_LIMIT instead of a literal); update any relevant
documentation/comments and ensure $criteria->setLimit($limit) receives the
clamped value.

Comment on lines +7 to +9
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

PHPUnit 9.6 attribute support for Test and DataProvider

💡 Result:

PHPUnit 9.6 does not support PHP 8 attributes such as #[Test] or #[DataProvider] for marking test methods or wiring data providers. In 9.6 you must use:

  • test method naming convention test* or the docblock annotation @test to mark a method as a test. [1]
  • the docblock annotation @dataProvider to attach a data provider to a test method. [2]

Attribute-based equivalents like PHPUnit\Framework\Attributes\DataProvider (and other PHPUnit\Framework\Attributes\*) are documented for PHPUnit 10+. [3]

Sources
[1] PHPUnit 9.6 manual – “Writing Tests for PHPUnit” (@test annotation) (docs.phpunit.de)
[2] PHPUnit 9.6 manual – “Data Providers” (@dataProvider annotation) (docs.phpunit.de)
[3] PHPUnit 10 manual – “Attributes” (includes DataProvider(...) attribute) (docs.phpunit.de)

Citations:


PHPUnit attributes require version 10+; tests must work across 9.6, 10.5, and 11.x.

PHP 8 attributes (#[Test], #[DataProvider]) are not recognized by PHPUnit 9.6. Add docblock annotations to maintain compatibility:

Required fix
-    #[Test]
-    #[DataProvider('hardenedFilesProvider')]
+    /**
+     * `@test`
+     * `@dataProvider` hardenedFilesProvider
+     */
     public function noHashlessRequestCalls(string $filePath): void

Apply to both noHashlessRequestCalls and noElvisOperatorOnDualSourceRequestCalls. Remove the unused Attributes\Test and Attributes\DataProvider imports once docblock annotations are in place.

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

In `@tests/unit/htdocs/hardening/RequestHashPinningTest.php` around lines 7 - 9,
The tests use PHPUnit 10 attributes which break compatibility with PHPUnit 9.6;
update the two test methods noHashlessRequestCalls and
noElvisOperatorOnDualSourceRequestCalls to use docblock annotations (`@test` and
`@dataProvider`) instead of #[Test] and #[DataProvider], remove the now-unused use
statements for PHPUnit\Framework\Attributes\Test and
PHPUnit\Framework\Attributes\DataProvider, and ensure the data provider names
referenced in the docblocks match the existing provider methods.

Comment on lines +54 to +106
#[Test]
#[DataProvider('hardenedFilesProvider')]
public function noHashlessRequestCalls(string $filePath): void
{
if (!file_exists($filePath)) {
self::markTestSkipped("File not found: {$filePath}");
}

$source = file_get_contents($filePath);
self::assertNotFalse($source, "Failed to read file: {$filePath}");
$lines = explode("\n", $source);
$violations = [];

foreach ($lines as $lineNum => $line) {
// Skip comments
$trimmed = ltrim($line);
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '/*')) {
continue;
}

// Find all Request::getXxx( occurrences and extract balanced args
if (preg_match_all(self::CALL_START_PATTERN, $line, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $matchInfo) {
$fullMatch = $matchInfo[0];
$offset = $matchInfo[1];
// Find the opening paren position
$parenPos = $offset + strlen($fullMatch) - 1;
// Extract balanced argument string
$inner = self::extractBalancedArgs($line, $parenPos);
if ($inner === null) {
continue;
}
$argCount = self::countArguments($inner);

// Calls with <= 2 args are missing the hash (3rd arg)
if ($argCount <= 2) {
$callText = substr($line, $offset, strlen($fullMatch) + strlen($inner) + 1);
$violations[] = sprintf(' Line %d: %s', $lineNum + 1, trim($callText));
}
}
}
}

self::assertEmpty(
$violations,
sprintf(
"Found %d hashless Request::getXxx() call(s) in %s:\n%s",
count($violations),
basename($filePath),
implode("\n", $violations),
),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider reducing complexity in noHashlessRequestCalls.

SonarCloud flags cognitive complexity of 18 (threshold: 15). While this is test code and the parsing logic is inherently branchy, you could extract the inner loop body into a helper method for clarity.

♻️ Optional refactor to reduce nesting
+    /**
+     * Check a single line for hashless Request calls.
+     * `@return` array<string> Violations found on this line
+     */
+    private static function checkLineForHashlessCalls(string $line, int $lineNum): array
+    {
+        $violations = [];
+        if (preg_match_all(self::CALL_START_PATTERN, $line, $matches, PREG_OFFSET_CAPTURE)) {
+            foreach ($matches[0] as $matchInfo) {
+                $fullMatch = $matchInfo[0];
+                $offset    = $matchInfo[1];
+                $parenPos = $offset + strlen($fullMatch) - 1;
+                $inner = self::extractBalancedArgs($line, $parenPos);
+                if ($inner === null) {
+                    continue;
+                }
+                $argCount = self::countArguments($inner);
+                if ($argCount <= 2) {
+                    $callText = substr($line, $offset, strlen($fullMatch) + strlen($inner) + 1);
+                    $violations[] = sprintf('  Line %d: %s', $lineNum + 1, trim($callText));
+                }
+            }
+        }
+        return $violations;
+    }
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 56-56: Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=XOOPS_XoopsCore25&issues=AZzj2e0NrypbsXvJSykT&open=AZzj2e0NrypbsXvJSykT&pullRequest=1650

🪛 PHPMD (2.15.0)

[warning] 56-106: The method noHashlessRequestCalls() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)

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

In `@tests/unit/htdocs/hardening/RequestHashPinningTest.php` around lines 54 -
106, The noHashlessRequestCalls method is over-complex; extract the inner
match-handling logic into a private helper to reduce nesting and cognitive
complexity. Create a helper (e.g., handleRequestCallMatch or
collectHashlessRequestCall) that accepts the original line, the match full text
and offset, the line number and a reference to $violations; move the parenPos
calculation, call to extractBalancedArgs, argCount via countArguments, and the
violation push (when argCount <= 2) into that helper. Replace the inner loop
body in noHashlessRequestCalls with a call to this helper for each match so the
main test reads as a simple loop over matches and delegates parsing/violation
logic to the new method; continue using CALL_START_PATTERN, extractBalancedArgs
and countArguments as-is.

Comment on lines +121 to +122
// Pattern: Request::getXxx(...) ?: Request::getXxx(...)
$elvisPattern = '/Request::(?:getString|getInt|getCmd|getWord|getFloat|getArray|getVar|getBool|getEmail|getUrl|getText)\s*\([^)]*\)\s*\?:\s*Request::/';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Regex [^)]* may produce false negatives for nested parentheses.

The elvis pattern uses [^)]* which won't match calls containing nested parentheses (e.g., Request::getInt('id', max(0, $x), 'GET')). This could miss violations in edge cases.

For the current codebase this is likely fine, but worth documenting or hardening if you expect more complex argument expressions.

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

In `@tests/unit/htdocs/hardening/RequestHashPinningTest.php` around lines 121 -
122, The regex stored in $elvisPattern uses [^)]* which fails on nested
parentheses in argument lists (so Request::getInt(... max(0, $x) ...) will not
match); replace the naive [^)]* with a more robust pattern that allows nested
parentheses or a balanced group (e.g., use a non-greedy match with optional
nested-parentheses handling or a recursive pattern if PCRE is enabled) so the
pattern still targets the Request::getString|getInt|... call followed by ?:
Request::; update the $elvisPattern variable accordingly to ensure it matches
calls with complex arguments.

@mambax7 mambax7 merged commit e290a2f into XOOPS:master Mar 14, 2026
10 of 11 checks passed
@codecov

codecov Bot commented Mar 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (db29899) to head (2b4e4f5).
⚠️ Report is 134 commits behind head on master.

Additional details and impacted files
@@      Coverage Diff       @@
##   master   #1650   +/-   ##
==============================
==============================

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

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.

2 participants