Skip to content

8964 global front controller#2

Open
coding-sunshine wants to merge 29 commits intomasterfrom
8964-global-front-controller
Open

8964 global front controller#2
coding-sunshine wants to merge 29 commits intomasterfrom
8964-global-front-controller

Conversation

@coding-sunshine
Copy link
Copy Markdown
Owner

@coding-sunshine coding-sunshine commented Oct 4, 2025

Fixes #

Short description of what this resolves:

Changes proposed in this pull request:

Does your code include anything generated by an AI Engine? Yes / No

If you answered yes: Verify that each file that has AI generated code has a description that describes what AI engine was used and that the file includes AI generated code. Sections of code that are entirely or mostly generated by AI should be marked with a comment header and footer that includes the AI engine used and stating the code was AI.

Summary by CodeRabbit

  • New Features

    • Optional global front controller: centralized routing, multisite support, static-asset passthrough, lifecycle hooks, and runtime flags.
  • Security

    • Stricter path/access controls preventing direct include/config access and added runtime security checks; login flow gains frame-attack protections.
  • Documentation

    • Detailed front-controller guide with setup, configuration, extension points, testing, and rollback instructions.
  • Tests

    • New compatibility, security, and performance test suites and runnable scripts; CI steps added to run and upload front-controller results.
  • Chores

    • Standardized env sample to exported vars, updated ignore rules, autoload mapping extended, and test report artifacts included.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 4, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a global PHP front controller (home.php) with routing, security checks, event hooks, safe include execution, and shutdown dispatch; introduces a PSR‑4 front‑controller library under OpenCoreEMR (Router, RouteConfig, SecurityValidator, FrontControllerEvent); adds security-check.php, Apache rewrite rules, exported env vars, Composer mapping, PHPUnit suites, CI steps, tests, scripts, docs, and report artifacts. (50 words)

Changes

Cohort / File(s) Summary of changes
Front controller entry & docs
home.php, README-FRONT-CONTROLLER.md, security-check.php, .htaccess, .env.example
Add home.php front controller (route extraction, multisite support, safe include execution, early/late event dispatch, shutdown handling); add auto-prepended security-check.php; add Apache rewrite/front-controller rules; convert env lines to export form and add OPENEMR_ENABLE_FRONT_CONTROLLER / OPENEMR_FC_LOG_LEVEL; add README documentation.
Routing library (PSR‑4)
src/OpenCoreEMR/FrontController/*, composer.json
New OpenCoreEMR\FrontController library: Router, RouteConfig, RouteConfigInterface, SecurityValidator for route extraction, site-id handling, trailing-slash redirect handling, forbidden/admin/deprecated pattern checks, safe target resolution, include detection, and logging/debug helpers; add OpenCoreEMR\\ PSR‑4 mapping in composer.json.
Event object
src/Events/FrontController/FrontControllerEvent.php
Add FrontControllerEvent with EVENT_EARLY / EVENT_LATE constants and route/site/context accessors/mutators for lifecycle dispatch.
Security header hardening
interface/login/login.php, index.php
Add/update login header comments and set security headers (X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none'); minor formatting tweak in index.php.
CI / PHPUnit integration
.github/workflows/test.yml, phpunit.xml
Add FrontController test steps to GitHub Actions workflow and register a frontcontroller testsuite in phpunit.xml.
PHPUnit tests
tests/Tests/FrontController/*
Add Guzzle-based integration tests: CompatibilityTest and SecurityTest validating routing, multisite, API/portal endpoints, static asset passthrough, .inc.php blocking, path traversal prevention, security headers, feature-flag behavior, and producing JSON reports.
Shell test scripts & reports
tests/scripts/*, tests/reports/*, reports/.gitkeep, .gitignore
Add bash scripts for compatibility, security, and performance tests; add report artifacts and .gitkeep; add .gitignore rule for tests/reports/; create sample report files.
Minor edits
index.php
Small comment/formatting tweak (blank line after license); no control-flow changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User Agent
  participant A as Web Server (.htaccess)
  participant H as home.php (Front Controller)
  participant R as Router
  participant S as SecurityValidator
  participant T as Target PHP
  participant E as Event Dispatcher

  U->>A: HTTP request (URI)
  alt Static or existing file
    A-->>U: Serve static asset or existing controller
  else Rewritten to front controller
    A->>H: Rewrite to /home.php?_ROUTE=...
    H->>E: Dispatch front_controller.early (if available)
    H->>R: extractRoute() / determineSiteId()
    H->>S: validate include / forbidden / admin rules
    alt Blocked (.inc.php or forbidden)
      H-->>U: 403 or 404 response
      H->>E: Dispatch front_controller.late (error context)
    else Resolve target
      R-->>H: target path or null
      alt Not found
        H-->>U: 404 response
        H->>E: Dispatch front_controller.late (error context)
      else Include/execute target (adjust SCRIPT_* vars)
        H->>T: include target
        T-->>H: output/response
        H->>E: Dispatch front_controller.late (success context)
        H-->>U: Response
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Files/areas meriting extra attention:

  • home.php — path normalization, SCRIPT_* manipulation, exception/shutdown dispatch and event payloads.
  • Router / RouteConfig — pattern matching, realpath usage, baseDir handling, trailing-slash redirect logic.
  • .htaccess and security-check.php — deny/pass-through rules vs legitimate assets and web server compatibility.
  • PHPUnit tests and shell scripts (tests/Tests/FrontController/*, tests/scripts/*) — environment/network assumptions, flakiness, and report generation.
  • Composer autoload mapping — ensure autoloader and namespace collisions are correct.

Poem

I hop through routes with careful cheer,
I guard the burrow files I hold dear.
I block the sneaky .inc and prance,
Rewrite the paths, give requests a chance.
Early and late, I twitch my ear—front controller dance. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "8964 global front controller" directly and specifically describes the primary change in this changeset. The entire PR implements a global front controller system for OpenEMR, including routing logic, security validation, configuration management, and comprehensive test coverage. The title accurately reflects this main objective and is clear enough that a teammate scanning history would understand this PR introduces a front controller feature. While the title could be slightly more descriptive about the purpose or benefit (e.g., "for request routing"), it meets the core requirement of being specific, related to the actual changes, and clearly summarizing the primary change.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 8964-global-front-controller

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.

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: 17

♻️ Duplicate comments (1)
tests/reports/compatibility-test-report.json (1)

1-5: Duplicate: Test artifacts should not be committed.

Same issue as security-test-report.json - this generated test artifact should be excluded from version control.

See comment on tests/reports/security-test-report.json for the recommended approach.

🧹 Nitpick comments (13)
tests/scripts/test_performance.sh (2)

94-101: Add timeout to curl/ab commands to prevent hanging.

The Apache Bench command lacks a timeout, which could cause the script to hang indefinitely if the server is unresponsive. This is particularly problematic in CI/CD pipelines.

Apply this diff to add a timeout:

-    if ! ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1); then
+    if ! ab_output=$(timeout 60 ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1); then
         {
             printf "%sERROR: Apache Bench failed for %s%s\n" "$RED" "$url" "$NC"
             echo "Output: $ab_output"

272-291: Resource usage detection may fail silently on some systems.

The script attempts to detect PHP processes and CPU usage but the methods used may not be portable across all environments:

  • Line 273: grep -c '[p]hp' may count unrelated processes or miss PHP-FPM processes
  • Lines 281-286: Different top syntax between macOS/Linux could fail silently with || echo "N/A"

Consider more robust process detection:

# More specific PHP process detection
php_procs=$(ps aux | grep -E '[p]hp(-fpm|-cgi)?(\s|$)' | grep -v grep | wc -l)

Alternatively, document these as "best effort" metrics in the report.

tests/reports/security-test-report.json (1)

1-6: Consider excluding test report artifacts from version control.

Test reports like this JSON file are typically generated during test execution and should not be committed to the repository. They can become stale, create merge conflicts, and increase repository size over time.

Add test reports to .gitignore:

+# Test reports
+tests/reports/*.json
+tests/reports/*.txt

If you need example reports for documentation, consider:

  1. Keeping them in a separate examples/ directory
  2. Documenting the expected format in README files
  3. Generating them during CI/CD only
tests/scripts/test_compatibility.sh (1)

56-56: Add timeout and connection limits to curl commands.

The curl commands lack timeouts, which could cause the script to hang indefinitely if a server is unresponsive. This is problematic in CI/CD environments.

Apply these diffs:

     # Make HTTP request and get status code
-    http_code=$(curl -s -o /dev/null -w "%{http_code}" -L "${url}")
+    http_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 --connect-timeout 10 "${url}")
 http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+    --max-time 30 --connect-timeout 10 \
     -d "authUser=test&authPass=test" \
     "${BASE_URL}/interface/login/login.php")

Also applies to: 221-223

README-FRONT-CONTROLLER.md (1)

28-490: Add fenced-code languages to satisfy markdownlint.

CI lint (MD040) flags the many blocks without a language. Tag them (`bash, ```php, ```apache`, etc.) to keep docs lint-clean.

src/OpenCoreEMR/FrontController/Router.php (1)

38-44: Validate the _ROUTE parameter to prevent potential issues.

The route is extracted from $_GET['_ROUTE'] without validation. While strtok() removes query strings, the route itself could contain malicious or malformed input that might cause issues downstream.

Consider adding basic input validation:

 public function extractRoute(): string
 {
     $route = $_GET['_ROUTE'] ?? '';
+    
+    // Basic validation: ensure route is a string and not empty after trimming
+    $route = trim((string) $route);
 
     // Remove query string
     $route = strtok($route, '?');
 
     $this->route = $route;
     return $route;
 }
src/OpenCoreEMR/FrontController/RouteConfig.php (3)

169-173: File extension validation is too restrictive.

The validation only allows .php files, but the docblock (lines 153-158) and the logic suggest this is for loading configuration, not executing arbitrary PHP. Consider if JSON or other safer formats should be supported.

If the intention is to only load PHP config arrays, the current implementation is acceptable. However, consider supporting JSON for better security:

// Validate file extension
$ext = strtolower(pathinfo($configPath, PATHINFO_EXTENSION));
if ($ext === 'php') {
    $config = require $realPath;
} elseif ($ext === 'json') {
    $json = file_get_contents($realPath);
    $config = json_decode($json, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        return false;
    }
} else {
    return false;
}

176-179: Redundant realpath() check after file_exists().

Line 165 already checks file_exists(), and if true, the path is valid. The realpath() check on line 176 will always succeed if file_exists() returned true (on most systems), making this check redundant.

The check is defensive programming and doesn't hurt, but could be simplified:

-        // Validate realpath to prevent path traversal
-        $realPath = realpath($configPath);
-        if ($realPath === false) {
-            return false;
-        }
+        // Use realpath for consistent path resolution
+        $realPath = realpath($configPath);

183-199: No validation of pattern array contents.

The code iterates through the config arrays and adds patterns without validating that they are valid regex patterns. Invalid patterns could cause preg_match() failures in matchesAnyPattern().

Add pattern validation:

         if (isset($config['forbidden']) && is_array($config['forbidden'])) {
             foreach ($config['forbidden'] as $pattern) {
+                // Validate regex pattern
+                if (@preg_match($pattern, '') === false) {
+                    continue; // Skip invalid patterns
+                }
                 $this->addForbiddenPattern($pattern);
             }
         }
 
         if (isset($config['admin']) && is_array($config['admin'])) {
             foreach ($config['admin'] as $pattern) {
+                if (@preg_match($pattern, '') === false) {
+                    continue;
+                }
                 $this->addAdminPattern($pattern);
             }
         }
 
         if (isset($config['deprecated']) && is_array($config['deprecated'])) {
             foreach ($config['deprecated'] as $pattern) {
+                if (@preg_match($pattern, '') === false) {
+                    continue;
+                }
                 $this->addDeprecatedPath($pattern);
             }
         }
tests/Tests/FrontController/CompatibilityTest.php (4)

31-36: HTTP client allows redirects by default, which may hide routing issues.

Setting allow_redirects to true in the default client configuration means tests may not catch redirect loops or incorrect redirect behavior. Individual tests override this, but the default could mask issues.

Consider setting redirects to false by default and explicitly enabling them in tests that need to follow redirects:

         self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
         self::$client = new Client([
             'base_uri' => self::$baseUrl,
             'http_errors' => false,
-            'allow_redirects' => true,
+            'allow_redirects' => false,
         ]);

169-180: Test assumes redirect behavior without verifying redirect target.

The test expects a 302 redirect but doesn't verify that the redirect is to the login page. A redirect to any location would pass this test.

Enhance the test to verify the redirect location:

         $response = self::$client->get('/interface/patient_file/summary/demographics.php', [
             'allow_redirects' => false
         ]);
         $httpCode = $response->getStatusCode();
+        $location = $response->getHeader('Location')[0] ?? '';
 
         // Should redirect to login (302) since not authenticated
         $this->assertEquals(
             302,
             $httpCode,
             'Patient file should redirect to login when not authenticated'
         );
+        $this->assertStringContainsString(
+            'login',
+            strtolower($location),
+            'Redirect should be to login page'
+        );

244-259: Test uses hardcoded credentials without cleanup.

The test posts hardcoded test credentials (authUser and authPass both set to 'test') without any cleanup. If these credentials exist in the test environment, this could create a session that affects subsequent tests.

Add a comment noting the limitation and consider using unique test credentials:

     public function testPostRequestsWork(): void
     {
+        // Note: Using invalid credentials to test POST handling without creating sessions
         $response = self::$client->post('/interface/login/login.php', [
             'form_params' => [
-                'authUser' => 'test',
-                'authPass' => 'test'
+                'authUser' => 'nonexistent_test_user_' . time(),
+                'authPass' => 'invalid_password_' . time()
             ],
             'allow_redirects' => false
         ]);

291-293: Report file path uses relative path that may fail in different contexts.

Using __DIR__ . '/../../reports/' assumes a specific directory structure relative to the test file. This could break if tests are run from different working directories or if the directory structure changes.

Use a more robust path resolution:

-        $reportPath = __DIR__ . '/../../reports/compatibility-test-report.json';
+        // Use a path relative to the project root or a configurable location
+        $projectRoot = dirname(__DIR__, 3); // Assumes tests are in tests/Tests/FrontController/
+        $reportPath = $projectRoot . '/tests/reports/compatibility-test-report.json';
         mkdir(dirname($reportPath), 0755, true);
         file_put_contents($reportPath, json_encode($report, JSON_PRETTY_PRINT) . "\n");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13eacc6 and 6cde1e8.

📒 Files selected for processing (22)
  • .env.example (1 hunks)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • .htaccess (1 hunks)
  • README-FRONT-CONTROLLER.md (1 hunks)
  • composer.json (1 hunks)
  • home.php (1 hunks)
  • phpunit.xml (2 hunks)
  • reports/.gitkeep (1 hunks)
  • security-check.php (1 hunks)
  • src/Events/FrontController/FrontControllerEvent.php (1 hunks)
  • src/OpenCoreEMR/FrontController/RouteConfig.php (1 hunks)
  • src/OpenCoreEMR/FrontController/RouteConfigInterface.php (1 hunks)
  • src/OpenCoreEMR/FrontController/Router.php (1 hunks)
  • src/OpenCoreEMR/FrontController/SecurityValidator.php (1 hunks)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
  • tests/reports/compatibility-test-report.json (1 hunks)
  • tests/reports/security-test-report.json (1 hunks)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
src/OpenCoreEMR/FrontController/SecurityValidator.php (1)
library/classes/thumbnail/ThumbnailGenerator.php (1)
  • error_log (212-216)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (45-70)
home.php (3)
src/OpenCoreEMR/FrontController/Router.php (8)
  • Router (20-129)
  • determineSiteId (50-57)
  • extractRoute (36-45)
  • handleTrailingSlash (64-71)
  • isForbiddenPath (76-79)
  • requiresAdmin (84-87)
  • resolveTargetFile (92-112)
  • getSiteId (125-128)
src/OpenCoreEMR/FrontController/SecurityValidator.php (4)
  • SecurityValidator (20-53)
  • logSecurityEvent (41-44)
  • isIncludeFile (33-36)
  • getDebugMessage (49-52)
src/Events/FrontController/FrontControllerEvent.php (2)
  • FrontControllerEvent (23-136)
  • getSiteId (86-89)
src/OpenCoreEMR/FrontController/Router.php (3)
src/Events/FrontController/FrontControllerEvent.php (3)
  • __construct (64-69)
  • getRoute (76-79)
  • getSiteId (86-89)
src/OpenCoreEMR/FrontController/RouteConfig.php (5)
  • __construct (43-46)
  • RouteConfig (20-233)
  • isDeprecated (145-148)
  • isForbidden (123-126)
  • requiresAdmin (134-137)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (3)
  • isDeprecated (65-65)
  • isForbidden (49-49)
  • requiresAdmin (57-57)
src/Events/FrontController/FrontControllerEvent.php (2)
src/OpenCoreEMR/FrontController/RouteConfig.php (1)
  • __construct (43-46)
src/OpenCoreEMR/FrontController/Router.php (3)
  • __construct (27-31)
  • getRoute (117-120)
  • getSiteId (125-128)
src/OpenCoreEMR/FrontController/RouteConfig.php (3)
src/Events/FrontController/FrontControllerEvent.php (1)
  • __construct (64-69)
src/OpenCoreEMR/FrontController/Router.php (2)
  • __construct (27-31)
  • requiresAdmin (84-87)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (5)
  • addDeprecatedPath (41-41)
  • isForbidden (49-49)
  • requiresAdmin (57-57)
  • isDeprecated (65-65)
  • loadFromFile (73-73)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-39)
security-check.php (1)
library/classes/thumbnail/ThumbnailGenerator.php (1)
  • error_log (212-216)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (2)
src/OpenCoreEMR/FrontController/RouteConfig.php (5)
  • addDeprecatedPath (97-100)
  • isForbidden (123-126)
  • requiresAdmin (134-137)
  • isDeprecated (145-148)
  • loadFromFile (163-202)
src/OpenCoreEMR/FrontController/Router.php (1)
  • requiresAdmin (84-87)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (43-68)
tests/Tests/FrontController/SecurityTest.php (2)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-37)
src/Common/Http/HttpRestRequest.php (2)
  • hasHeader (225-228)
  • getHeaderLine (697-700)
🪛 markdownlint-cli2 (0.18.1)
README-FRONT-CONTROLLER.md

10-10: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (11)
.github/workflows/test.yml (1)

344-355: FrontController test integration looks consistent.

The new build_test frontcontroller step and Codecov upload mirror the existing suite pattern and slot cleanly into the workflow. No issues from my side.

.gitignore (1)

31-31: Ignore generated test reports

Ignoring tests/reports/ keeps transient artifacts out of version control. Change looks good.

.env.example (1)

7-12: LGTM! Clear documentation for optional front controller feature.

The new environment variables are well-documented with clear explanations of their purpose and expected values. The optional nature and default behavior (disabled) are appropriately communicated.

tests/scripts/test_performance.sh (2)

1-50: LGTM! Well-structured script setup with proper error handling.

The script initialization is solid:

  • Proper shebang and set -e for error handling
  • Clear configuration with sensible defaults
  • Good user feedback with color-coded output
  • Proactive check for Apache Bench availability with installation instructions

226-246: LGTM! Robust numeric validation before calculations.

Good defensive programming with:

  • Validation of numeric values using regex
  • Clear warning message when validation fails
  • Proper use of bc for portable floating-point arithmetic
phpunit.xml (1)

30-30: LGTM! Consistent test suite additions.

The new frontcontroller test suite is properly integrated:

  • Added to the main openemr suite for comprehensive test runs
  • Available as a standalone suite for targeted testing
  • Follows the existing naming and structure conventions

Also applies to: 59-61

composer.json (1)

190-191: LGTM! Proper PSR-4 autoload configuration.

The new OpenCoreEMR namespace is correctly mapped to src/OpenCoreEMR, enabling autoloading for the new front controller components. The mapping follows PSR-4 standards and coexists properly with the existing OpenEMR namespace.

tests/scripts/test_compatibility.sh (3)

1-70: LGTM! Clean script structure with good organization.

The script setup is well-designed:

  • Proper error handling with set -e
  • Clear configuration and reporting
  • Reusable run_test function that handles multiple expected codes
  • Color-coded output for better readability

178-194: LGTM! Graceful handling of optional test dependencies.

Good defensive programming:

  • Checks for the existence of the public directory before running static asset tests
  • Provides informative skip message
  • Prevents false failures when directory structure varies

277-293: LGTM! Clear success/failure reporting with actionable guidance.

The final summary is excellent:

  • Color-coded pass/fail status
  • Contextual guidance based on results
  • Proper exit codes for CI/CD integration
  • Encourages review of failures
tests/Tests/FrontController/CompatibilityTest.php (1)

86-97: LGTM: REST API test correctly validates routing without authentication.

The test appropriately checks that the REST API endpoint returns 401 (unauthorized), which confirms that routing is working and authentication is required. This is a good way to test routing without needing valid credentials.

Comment thread .env.example
@@ -1,4 +1,12 @@
#!/bin/sh
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

Remove the shebang line from .env.example.

A .env file is a data file loaded by libraries like phpdotenv, not an executable script. The shebang #!/bin/sh serves no purpose here and could confuse developers about the file's role. Environment variable files should contain only variable definitions and comments.

Apply this diff:

-#!/bin/sh
-
 # Possible options are prod or dev. No spaces, no quotes.
📝 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
#!/bin/sh
# Possible options are prod or dev. No spaces, no quotes.
🤖 Prompt for AI Agents
In .env.example around lines 1 to 1, remove the shebang line ("#!/bin/sh")
because .env files are not executable scripts; edit the file to delete that
first line so it contains only environment variable declarations and comments,
saving the file without adding any new executable directives.

Comment thread .htaccess Outdated
Comment thread .htaccess Outdated
Comment thread home.php Outdated
Comment thread home.php Outdated
Comment thread src/OpenCoreEMR/FrontController/Router.php Outdated
Comment thread src/OpenCoreEMR/FrontController/SecurityValidator.php
Comment thread tests/scripts/test_performance.sh Outdated
Comment on lines +104 to +127
requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output" || echo "0")
time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output" || echo "0")
failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output" || echo "0")

# Extract percentiles using awk with safe defaults
p50=$(awk '/50%/ {print $2}' <<< "$ab_output" || echo "0")
p95=$(awk '/95%/ {print $2}' <<< "$ab_output" || echo "0")
p99=$(awk '/99%/ {print $2}' <<< "$ab_output" || echo "0")

# Display results
{
echo "Results:"
echo " Requests/sec: $requests_per_sec"
echo " Time/request: ${time_per_request}ms (mean)"
echo " Failed requests: $failed_requests"
echo " 50th percentile: ${p50}ms"
echo " 95th percentile: ${p95}ms"
echo " 99th percentile: ${p99}ms"
echo
} | tee -a "$REPORT_FILE"

# Return metrics as array (for comparison)
echo "$requests_per_sec|$time_per_request|$p95"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Verify that metric extraction handles edge cases correctly.

The awk commands use || echo "0" as fallback values when parsing fails. While this prevents script failures, it could mask legitimate parsing errors or unexpected ab output formats, leading to misleading "0" values in the report.

Consider adding validation to distinguish between:

  • Legitimate zero values (e.g., no failed requests)
  • Parsing failures due to unexpected output format
  • Network/server errors that prevent ab from running

For example, check if the ab_output contains expected patterns before extracting metrics:


🏁 Script executed:

# Verify ab output contains expected sections
if ! grep -q "Requests per second" <<< "$ab_output"; then
    echo "WARNING: Unexpected ab output format" | tee -a "$REPORT_FILE"
fi

Length of output: 39


Validate ab exit code and output format before parsing metrics
Add a POSIX-compatible pre-check of the ab command and ensure expected sections exist. For example:

# Run ab and capture its exit code and output
ab_output=$(ab "$AB_OPTS" 2>&1) || ab_failed=1
ab_exit_code=${ab_failed:-0}

# Verify ab succeeded and output contains "Requests per second"
if [ "$ab_exit_code" -ne 0 ] || ! printf '%s\n' "$ab_output" | grep -q "Requests per second"; then
  echo "WARNING: ab failed (exit code $ab_exit_code) or unexpected output format" | tee -a "$REPORT_FILE"
  return 1
fi

# Safe parsing with pipe (no here-string)
requests_per_sec=$(printf '%s\n' "$ab_output" | awk '/Requests per second/ {print $4}')
time_per_request=$(printf '%s\n' "$ab_output" | awk '/Time per request.*mean\)/ {print $4; exit}')
failed_requests=$(printf '%s\n' "$ab_output" | awk '/Failed requests/ {print $3}')

# Percentiles
p50=$(printf '%s\n' "$ab_output" | awk '/50%/ {print $2}')
p95=$(printf '%s\n' "$ab_output" | awk '/95%/ {print $2}')
p99=$(printf '%s\n' "$ab_output" | awk '/99%/ {print $2}')

This ensures you distinguish between:

  • ab execution failures
  • Missing or malformed sections in its output
  • Legitimate zero values when parsing succeeds
🤖 Prompt for AI Agents
tests/scripts/test_performance.sh lines 104-127: the script currently parses ab
output unconditionally using here-strings and fallbacks, which can mask ab
failures or malformed output; update the code to capture ab's exit code and
stdout/stderr (e.g., ab_output=$(ab "$AB_OPTS" 2>&1; ab_exit=$?; export
ab_exit)), then check that ab_exit is zero and that ab_output contains the
expected marker like "Requests per second" (log a warning to $REPORT_FILE and
return 1 if not), and after that parse metrics using printf '%s\n' "$ab_output"
| awk ... (replace here-strings and the || echo "0" fallbacks) so parsing only
happens on validated output and legitimate zero values are preserved.

Comment thread tests/scripts/test_security.sh Outdated
Comment on lines +41 to +54
private static function loadVulnerableIncFiles(): void
{
$basePath = dirname(__DIR__, 2);
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($basePath)
);

foreach ($iterator as $file) {
if ($file->isFile() && preg_match('/\.inc\.php$/i', $file->getFilename())) {
$relativePath = str_replace($basePath . '/', '', $file->getPathname());
self::$vulnerable_inc_files[] = $relativePath;
}
}
}
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

Test never inspects production .inc.php files.

dirname(__DIR__, 2) resolves to the tests/ folder, so $iterator never touches the real application tree. self::$vulnerable_inc_files stays empty, the loop skips, and the test can’t catch regressions. Walk up one more level (repo root) before iterating.

-        $basePath = dirname(__DIR__, 2);
+        $basePath = dirname(__DIR__, 3);
📝 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
private static function loadVulnerableIncFiles(): void
{
$basePath = dirname(__DIR__, 2);
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($basePath)
);
foreach ($iterator as $file) {
if ($file->isFile() && preg_match('/\.inc\.php$/i', $file->getFilename())) {
$relativePath = str_replace($basePath . '/', '', $file->getPathname());
self::$vulnerable_inc_files[] = $relativePath;
}
}
}
private static function loadVulnerableIncFiles(): void
{
$basePath = dirname(__DIR__, 3);
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($basePath)
);
foreach ($iterator as $file) {
if ($file->isFile() && preg_match('/\.inc\.php$/i', $file->getFilename())) {
$relativePath = str_replace($basePath . '/', '', $file->getPathname());
self::$vulnerable_inc_files[] = $relativePath;
}
}
}
🤖 Prompt for AI Agents
In tests/Tests/FrontController/SecurityTest.php around lines 41–54, the test
sets $basePath = dirname(__DIR__, 2) which resolves to the tests/ directory so
the RecursiveDirectoryIterator never scans the application tree; change the base
path to walk up one more level (e.g. dirname(__DIR__, 3) or realpath(__DIR__ .
'/../../..')) so $iterator starts at the repository root and the loop can
collect production .inc.php files into self::$vulnerable_inc_files.

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

♻️ Duplicate comments (2)
tests/Tests/FrontController/SecurityTest.php (1)

41-55: Scan the real codebase, not just tests/

dirname(__DIR__, 2) lands inside the tests directory, so the iterator never reaches production .inc.php files—self::$vulnerable_inc_files stays empty and the test vacuously passes. Walk up one more level to the repo root.

-        $basePath = dirname(__DIR__, 2);
+        $basePath = dirname(__DIR__, 3);
src/OpenCoreEMR/FrontController/RouteConfig.php (1)

110-115: Replace PHP 8.4-only array_any()

array_any() doesn’t exist before PHP 8.4. With our composer constraint at 8.2, hitting isForbidden()/requiresAdmin()/isDeprecated() will fatal. Use a simple loop instead.

Apply this diff:

-        return array_any(
-            $patterns,
-            fn(string $pattern): bool => (bool) preg_match($pattern, $route)
-        );
+        foreach ($patterns as $pattern) {
+            if (preg_match($pattern, $route)) {
+                return true;
+            }
+        }
+
+        return false;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cde1e8 and 90cd613.

📒 Files selected for processing (8)
  • .htaccess (1 hunks)
  • home.php (1 hunks)
  • security-check.php (1 hunks)
  • src/OpenCoreEMR/FrontController/RouteConfig.php (1 hunks)
  • src/OpenCoreEMR/FrontController/Router.php (1 hunks)
  • src/OpenCoreEMR/FrontController/SecurityValidator.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/OpenCoreEMR/FrontController/SecurityValidator.php
  • src/OpenCoreEMR/FrontController/Router.php
  • .htaccess
🧰 Additional context used
🧬 Code graph analysis (5)
src/OpenCoreEMR/FrontController/RouteConfig.php (3)
src/OpenCoreEMR/FrontController/Router.php (2)
  • __construct (27-35)
  • requiresAdmin (91-94)
src/Events/FrontController/FrontControllerEvent.php (1)
  • __construct (64-69)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (5)
  • addDeprecatedPath (41-41)
  • isForbidden (49-49)
  • requiresAdmin (57-57)
  • isDeprecated (65-65)
  • loadFromFile (73-73)
home.php (3)
src/OpenCoreEMR/FrontController/Router.php (8)
  • Router (20-136)
  • determineSiteId (54-61)
  • extractRoute (40-49)
  • handleTrailingSlash (68-78)
  • isForbiddenPath (83-86)
  • requiresAdmin (91-94)
  • resolveTargetFile (99-119)
  • getSiteId (132-135)
src/OpenCoreEMR/FrontController/SecurityValidator.php (4)
  • SecurityValidator (20-53)
  • logSecurityEvent (41-44)
  • isIncludeFile (33-36)
  • getDebugMessage (49-52)
src/Events/FrontController/FrontControllerEvent.php (2)
  • FrontControllerEvent (23-136)
  • getSiteId (86-89)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (45-70)
security-check.php (1)
library/classes/thumbnail/ThumbnailGenerator.php (1)
  • error_log (212-216)
tests/Tests/FrontController/SecurityTest.php (2)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-37)
src/Common/Http/HttpRestRequest.php (2)
  • hasHeader (225-228)
  • getHeaderLine (697-700)
🪛 GitHub Actions: ShellCheck
tests/scripts/test_security.sh

[info] 20-20: Prefer putting braces around variable references even when not strictly required.


[info] 40-40: Consider invoking this command separately to avoid masking its return value (or use || true to ignore).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: Rector PHP Analysis (8.4)
  • GitHub Check: phpstan (8.4)
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests

Comment thread security-check.php
Comment on lines +17 to +24
// Block .inc.php and .config.php files, and any PHP under /includes/ or /inc/
if (preg_match('/\.(?:inc|config)\.php$/i', $requestedFile) || preg_match('#/(?:includes?|inc)/#i', $requestedFile)) {
http_response_code(403);
// Sanitize: remove control chars, limit length, preserve only filename
$sanitizedFile = basename(preg_replace('/[\x00-\x1F\x7F]/', '', $requestedFile));
$sanitizedFile = substr($sanitizedFile, 0, 255); // Limit length
error_log("OpenEMR Security: Blocked .inc.php access attempt: " . $sanitizedFile);
exit('Access Denied');
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 | 🔴 Critical

Block raw .inc requests too.

The guard only matches *.inc.php/.config.php. A direct hit on *.inc still leaks the include file (served as plain text). Align with the front controller check and the new tests by rejecting both .inc and .inc.php.

Apply this diff:

-if (preg_match('/\.(?:inc|config)\.php$/i', $requestedFile) || preg_match('#/(?:includes?|inc)/#i', $requestedFile)) {
+if (
+    preg_match('/\.inc(?:\.php)?$/i', $requestedFile) ||
+    preg_match('/\.config\.php$/i', $requestedFile) ||
+    preg_match('#/(?:includes?|inc)/#i', $requestedFile)
+) {
📝 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
// Block .inc.php and .config.php files, and any PHP under /includes/ or /inc/
if (preg_match('/\.(?:inc|config)\.php$/i', $requestedFile) || preg_match('#/(?:includes?|inc)/#i', $requestedFile)) {
http_response_code(403);
// Sanitize: remove control chars, limit length, preserve only filename
$sanitizedFile = basename(preg_replace('/[\x00-\x1F\x7F]/', '', $requestedFile));
$sanitizedFile = substr($sanitizedFile, 0, 255); // Limit length
error_log("OpenEMR Security: Blocked .inc.php access attempt: " . $sanitizedFile);
exit('Access Denied');
// Block .inc, .inc.php, .config.php files, and any PHP under /includes/ or /inc/
if (
preg_match('/\.inc(?:\.php)?$/i', $requestedFile) ||
preg_match('/\.config\.php$/i', $requestedFile) ||
preg_match('#/(?:includes?|inc)/#i', $requestedFile)
) {
http_response_code(403);
// Sanitize: remove control chars, limit length, preserve only filename
$sanitizedFile = basename(preg_replace('/[\x00-\x1F\x7F]/', '', $requestedFile));
$sanitizedFile = substr($sanitizedFile, 0, 255); // Limit length
error_log("OpenEMR Security: Blocked .inc.php access attempt: " . $sanitizedFile);
exit('Access Denied');
}
🤖 Prompt for AI Agents
In security-check.php around lines 17 to 24, the current guard only blocks
*.inc.php and .config.php but not raw *.inc files; update the first preg_match
to reject both .inc and the existing .inc.php/.config.php variants by replacing
the pattern with a single combined regex that matches either a trailing ".inc"
or the ".inc.php"/".config.php" forms (keep the i flag), leaving the rest of the
block (sanitization, logging, exit) intact.

Comment on lines +15 to +44
set -e

# Configuration
BASE_URL="${1:-http://localhost/openemr}"
REPORT_DIR="$(dirname "$0")/../../reports"
REPORT_FILE="$REPORT_DIR/security-test-report-$(date +%Y%m%d-%H%M%S).txt"

# Colors for output using tput (with fallback for non-interactive environments)
if command -v tput >/dev/null 2>&1 && [ -n "$TERM" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
YELLOW=""
NC=""
fi

# Create report directory
mkdir -p "$REPORT_DIR"

{
echo "Security Test Report"
echo "Date: $(date)"
echo "URL: $BASE_URL"
echo
} | tee "$REPORT_FILE"

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

Honor pipeline failures (fix ShellCheck).

ShellCheck is failing because set -e + | tee masks upstream errors. Add set -o pipefail so any command in the pipeline aborts the script, and keep CI green.

-set -e
+set -e
+set -o pipefail

(While you’re here, wrap bare variables like "$BASE_URL" in braces to silence the remaining warning.) Based on pipeline failures

📝 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
set -e
# Configuration
BASE_URL="${1:-http://localhost/openemr}"
REPORT_DIR="$(dirname "$0")/../../reports"
REPORT_FILE="$REPORT_DIR/security-test-report-$(date +%Y%m%d-%H%M%S).txt"
# Colors for output using tput (with fallback for non-interactive environments)
if command -v tput >/dev/null 2>&1 && [ -n "$TERM" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
YELLOW=""
NC=""
fi
# Create report directory
mkdir -p "$REPORT_DIR"
{
echo "Security Test Report"
echo "Date: $(date)"
echo "URL: $BASE_URL"
echo
} | tee "$REPORT_FILE"
set -e
set -o pipefail
# Configuration
BASE_URL="${1:-http://localhost/openemr}"
REPORT_DIR="$(dirname "$0")/../../reports"
REPORT_FILE="$REPORT_DIR/security-test-report-$(date +%Y%m%d-%H%M%S).txt"
# Colors for output using tput (with fallback for non-interactive environments)
if command -v tput >/dev/null 2>&1 && [ -n "$TERM" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
YELLOW=""
NC=""
fi
# Create report directory
mkdir -p "$REPORT_DIR"
{
echo "Security Test Report"
echo "Date: $(date)"
echo "URL: $BASE_URL"
echo
} | tee "$REPORT_FILE"
🧰 Tools
🪛 GitHub Actions: ShellCheck

[info] 20-20: Prefer putting braces around variable references even when not strictly required.


[info] 40-40: Consider invoking this command separately to avoid masking its return value (or use || true to ignore).

🤖 Prompt for AI Agents
In tests/scripts/test_security.sh around lines 15 to 44, the script uses set -e
but not pipefail which allows failures in earlier pipeline stages to be masked
by commands like | tee; add set -o pipefail immediately after set -e so any
failing command in a pipeline causes the script to exit, and update any bare
variable expansions to use braced forms (e.g. "${BASE_URL}", "${REPORT_DIR}",
"${REPORT_FILE}") throughout this block to satisfy ShellCheck and avoid
word-splitting issues.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Summary

This PR implements a comprehensive Global Front Controller security feature for OpenEMR. The change introduces a centralized request routing system that intercepts all PHP requests through a new `home.php` front controller, providing enhanced security controls while maintaining backward compatibility.

Core Architecture Changes:

  • Front Controller Pattern: All PHP requests are now routed through home.php when the feature is enabled via OPENEMR_ENABLE_FRONT_CONTROLLER=1
  • Security Layer: Blocks direct access to .inc.php files (which were causing fatal errors when accessed directly) and prevents path traversal attacks
  • Event System: Introduces FrontControllerEvent for module extensibility with early/late request lifecycle hooks
  • Route Configuration: Implements RouteConfigInterface with forbidden/admin/deprecated path management using regex patterns
  • Multisite Support: Preserves existing multisite functionality while adding centralized routing

Security Enhancements:

  • Prevents direct access to include files that weren't designed as web entry points
  • Implements path traversal protection and directory restriction controls
  • Adds comprehensive security headers and validation
  • Includes multiple defense layers: Apache .htaccess rules, PHP-level blocking via security-check.php, and front controller validation

Backward Compatibility:

  • Feature flag controlled (OPENEMR_ENABLE_FRONT_CONTROLLER) with transparent pass-through when disabled
  • Preserves all existing entry points and workflows
  • Maintains context manipulation so target files see correct __FILE__ and __DIR__ values
  • Extensive pass-through rules for static assets and existing front controllers

Testing Infrastructure:

  • Comprehensive test suites covering compatibility, security, and performance
  • Automated report generation for CI/CD integration
  • Shell script validation alongside PHPUnit tests

New Namespace Structure:

  • Adds OpenCoreEMR\ namespace under src/OpenCoreEMR for core front controller infrastructure
  • Separates architectural concerns from existing OpenEMR classes

This implementation follows modern PHP framework patterns (similar to Symfony) and addresses real security vulnerabilities where direct file access was causing system errors and potential security exposure.

PR Description Notes:

  • Missing issue number reference ("Fixes #" is empty)
  • Missing description of what the PR resolves
  • Missing list of proposed changes
  • Missing AI engine disclosure confirmation

Important Files Changed

Changed Files
Filename Score Overview
home.php 2/5 New global front controller with feature flag, security validation, event system integration, and routing logic - has critical bugs in feature flag check and interface mismatches
src/OpenCoreEMR/FrontController/RouteConfig.php 1/5 Route configuration implementation using PHP 8.4 array_any() function incompatible with required PHP 8.2, plus method naming mismatch with interface
src/OpenCoreEMR/FrontController/Router.php 2/5 Core routing class with path security validation and multisite support - contains security vulnerabilities in redirect handling and inconsistent path validation
src/OpenCoreEMR/FrontController/SecurityValidator.php 2/5 Security validation utilities for CLI detection and include file validation - has documentation mismatch and environment variable naming inconsistency
.htaccess 4/5 Comprehensive Apache configuration implementing front controller pattern with security blocking and pass-through rules
security-check.php 4/5 PHP security layer that blocks direct .inc.php file access with proper sanitization and logging
README-FRONT-CONTROLLER.md 4/5 Extensive documentation covering architecture, configuration, security features, testing, and troubleshooting
src/OpenCoreEMR/FrontController/RouteConfigInterface.php 4/5 Interface defining contract for routing configuration with security-critical path management methods
src/Events/FrontController/FrontControllerEvent.php 4/5 Event system for front controller extensibility following Symfony patterns with lifecycle hooks
tests/Tests/FrontController/CompatibilityTest.php 4/5 Comprehensive compatibility testing ensuring existing functionality remains intact during migration
tests/Tests/FrontController/SecurityTest.php 4/5 Security test suite validating .inc.php blocking, path traversal prevention, and security headers
tests/reports/security-test-report.json 4/5 Generated security test report documenting successful .inc.php file blocking validation
tests/reports/compatibility-test-report.json 4/5 Generated compatibility test report confirming preservation of core functionality
tests/scripts/test_performance.sh 4/5 Performance testing script comparing direct access vs front controller routing with Apache Bench
tests/scripts/test_compatibility.sh 4/5 Bash script for comprehensive backward compatibility testing of various entry points and workflows
tests/scripts/test_security.sh 4/5 Security testing script validating .inc.php blocking, path traversal prevention, and security headers
composer.json 5/5 Adds new PSR-4 autoloader namespace mapping for OpenCoreEMR\ to src/OpenCoreEMR directory
.github/workflows/test.yml 5/5 Adds FrontController testing step to CI workflow with Codecov integration
phpunit.xml 5/5 Integrates FrontController tests into both main test suite and dedicated isolated test suite
.env.example 3/5 Adds front controller configuration variables but includes incorrect shebang line for a config file
reports/.gitkeep 5/5 Standard .gitkeep file to preserve empty reports directory structure in version control
.gitignore 5/5 Excludes tests/reports/ directory from version control to prevent committing generated test artifacts

Confidence score: 2/5

  • This PR introduces significant architectural changes with several critical bugs and compatibility issues that could cause production failures
  • Score reflects serious implementation flaws including PHP version incompatibility, logic errors in core routing, security vulnerabilities, and interface mismatches
  • Pay close attention to RouteConfig.php (PHP 8.4 incompatibility), home.php (feature flag logic bug), Router.php (security vulnerabilities), and SecurityValidator.php (environment variable inconsistency)

Sequence Diagram

sequenceDiagram
    participant User
    participant WebServer as Web Server (.htaccess)
    participant HomeController as home.php
    participant Dotenv as Dotenv Library
    participant EventDispatcher as Event Dispatcher
    participant Router as Router
    participant RouteConfig as Route Config
    participant SecurityValidator as Security Validator
    participant TargetFile as Target PHP File

    User->>WebServer: "HTTP Request to /some/file.php"
    
    Note over WebServer: Check routing rules
    
    alt Static assets (CSS, JS, images)
        WebServer-->>User: "Serve directly (no routing)"
    else Existing front controllers (/apis/, /portal/, /oauth2/)
        WebServer-->>User: "Pass through to existing controllers"
    else .inc.php files
        WebServer-->>User: "403 Forbidden"
    else PHP files
        WebServer->>HomeController: "Route to home.php?_ROUTE=some/file.php"
    end
    
    HomeController->>Dotenv: "Load .env configuration"
    Dotenv-->>HomeController: "Environment variables loaded"
    
    alt Feature flag disabled
        Note over HomeController: OPENEMR_ENABLE_FRONT_CONTROLLER != 1
        HomeController->>SecurityValidator: "Check for .inc.php files"
        SecurityValidator-->>HomeController: "Security check result"
        alt .inc.php file detected
            HomeController-->>User: "403 Access Denied"
        else Valid PHP file
            HomeController->>TargetFile: "Include target file directly"
            TargetFile-->>User: "Response from target file"
        end
    else Feature flag enabled
        HomeController->>EventDispatcher: "Dispatch front_controller.early event"
        EventDispatcher-->>HomeController: "Early event processed"
        
        HomeController->>Router: "new Router(__DIR__)"
        Router-->>HomeController: "Router instance created"
        
        HomeController->>Router: "determineSiteId()"
        Router-->>HomeController: "Site ID determined"
        
        HomeController->>Router: "extractRoute()"
        Router-->>HomeController: "Route extracted from _ROUTE parameter"
        
        HomeController->>Router: "handleTrailingSlash()"
        alt Trailing slash redirect needed
            Router-->>User: "301 Redirect"
        else No redirect needed
            Router-->>HomeController: "Continue processing"
        end
        
        HomeController->>Router: "isForbiddenPath()"
        Router->>RouteConfig: "Check forbidden patterns"
        RouteConfig-->>Router: "Pattern match result"
        Router-->>HomeController: "Forbidden check result"
        
        alt Forbidden path
            HomeController->>SecurityValidator: "logSecurityEvent()"
            HomeController-->>User: "404 Not Found"
        else Path allowed
            HomeController->>Router: "requiresAdmin()"
            Router->>RouteConfig: "Check admin patterns"
            RouteConfig-->>Router: "Admin check result"
            Router-->>HomeController: "Admin requirement result"
            
            alt Admin required
                HomeController->>HomeController: "Set $_SERVER['REQUIRE_ADMIN'] = true"
            end
            
            HomeController->>SecurityValidator: "isIncludeFile(route)"
            SecurityValidator-->>HomeController: "Include file check result"
            
            alt .inc.php file
                HomeController->>SecurityValidator: "logSecurityEvent()"
                HomeController-->>User: "403 Access Denied"
            else Valid file
                HomeController->>Router: "resolveTargetFile()"
                Router-->>HomeController: "Target file path"
                
                alt Invalid or non-existent file
                    HomeController->>SecurityValidator: "logSecurityEvent()"
                    HomeController-->>User: "404 Not Found"
                else Valid target file
                    HomeController->>HomeController: "register_shutdown_function()"
                    HomeController->>TargetFile: "require targetFile"
                    TargetFile-->>User: "Response from target file"
                    HomeController->>EventDispatcher: "Dispatch front_controller.late event"
                end
            end
        end
    end
Loading

22 files reviewed, 16 comments

Edit Code Review Agent Settings | Greptile

Comment thread tests/scripts/test_performance.sh Outdated
evaluate_overhead() {
local overhead="$1"

if (( $(bc -l <<< "$overhead < 5") )); then
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Missing dependency check for bc command used in arithmetic comparisons

Suggested change
if (( $(bc -l <<< "$overhead < 5") )); then
# Check if bc (basic calculator) is available for floating point arithmetic
if ! command -v bc &> /dev/null; then
{
printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "$RED" "$NC"
echo " macOS: brew install bc"
echo " Linux: sudo apt-get install bc"
} | tee -a "$REPORT_FILE"
exit 1
fi
# Check if Apache Bench (ab) is available
if ! command -v ab &> /dev/null; then
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/scripts/test_performance.sh
Line: 57:57

Comment:
**logic:** Missing dependency check for `bc` command used in arithmetic comparisons

```suggestion
    # Check if bc (basic calculator) is available for floating point arithmetic
if ! command -v bc &> /dev/null; then
    {
        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "$RED" "$NC"
        echo "  macOS: brew install bc"
        echo "  Linux: sudo apt-get install bc"
    } | tee -a "$REPORT_FILE"
    exit 1
fi

# Check if Apache Bench (ab) is available
if ! command -v ab &> /dev/null; then
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread tests/scripts/test_performance.sh Outdated
# Calculate overhead
if [[ -n "$baseline_time" && -n "$fc_time" && "$baseline_time" != "0" ]]; then
# Validate numeric values
if ! [[ "$baseline_time" =~ ^[0-9]+\.?[0-9]*$ ]] || ! [[ "$fc_time" =~ ^[0-9]+\.?[0-9]*$ ]]; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Regex pattern doesn't handle negative numbers which could occur in performance measurements

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/scripts/test_performance.sh
Line: 228:228

Comment:
**logic:** Regex pattern doesn't handle negative numbers which could occur in performance measurements

How can I resolve this? If you propose a fix, please make it concise.

printf "Testing: Front controller responds when enabled... " | tee -a "$REPORT_FILE"

response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/home.php?_ROUTE=index.php")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

syntax: Environment variable name has typo - should be 'OPENEMR_ENABLE_FRONT_CONTROLLER' (single underscore)

Suggested change
echo " ℹ Front controller may be disabled or error occurred (OPENEMR_ENABLE_FRONT_CONTROLLER not set)" | tee -a "$REPORT_FILE"
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/scripts/test_security.sh
Line: 239:239

Comment:
**syntax:** Environment variable name has typo - should be 'OPENEMR_ENABLE_FRONT_CONTROLLER' (single underscore)

```suggestion
    echo "  ℹ Front controller may be disabled or error occurred (OPENEMR_ENABLE_FRONT_CONTROLLER not set)" | tee -a "$REPORT_FILE"
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +111 to +114
{
return array_any(
$patterns,
fn(string $pattern): bool => (bool) preg_match($pattern, $route)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: array_any() is not a native PHP function. Use array_filter() or a manual loop instead.

Suggested change
{
return array_any(
$patterns,
fn(string $pattern): bool => (bool) preg_match($pattern, $route)
foreach ($patterns as $pattern) {
if (preg_match($pattern, $route)) {
return true;
}
}
return false;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/OpenCoreEMR/FrontController/RouteConfig.php
Line: 111:114

Comment:
**logic:** `array_any()` is not a native PHP function. Use `array_filter()` or a manual loop instead.

```suggestion
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $route)) {
                return true;
            }
        }
        return false;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread security-check.php
<?php

/**
* Security check for include/config files and sensitive directories.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Comment doesn't match implementation - only blocks .inc.php files, not 'include/config files and sensitive directories' as originally intended

Suggested change
* Security check for include/config files and sensitive directories.
* Security check for .inc.php files.
*
* Auto-loaded before any PHP execution via .user.ini.
*
* @package OpenCoreEMR
* @link http://www.open-emr.org
* @author OpenCoreEMR, Inc.
* @copyright Copyright (c) 2025 OpenCoreEMR, Inc.
* @license GPLv3
Prompt To Fix With AI
This is a comment left during a code review.
Path: security-check.php
Line: 4:4

Comment:
**logic:** Comment doesn't match implementation - only blocks .inc.php files, not 'include/config files and sensitive directories' as originally intended

```suggestion
 * Security check for .inc.php files.
 *
 * Auto-loaded before any PHP execution via .user.ini.
 *
 * @package   OpenCoreEMR
 * @link      http://www.open-emr.org
 * @author    OpenCoreEMR, Inc.
 * @copyright Copyright (c) 2025 OpenCoreEMR, Inc.
 * @license   GPLv3
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread home.php
$targetFile = __DIR__ . '/' . ltrim($route, '/');

// Block .inc and .inc.php files even when front controller is disabled
if (preg_match('/\.inc(?:\.php)?$/i', $route)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Regex pattern only matches .inc.php but original change description mentions blocking .inc files too. Should use /\.inc(?:\.php)?$/i to match both patterns consistently

Suggested change
if (preg_match('/\.inc(?:\.php)?$/i', $route)) {
if (preg_match('/\.inc(?:\.php)?$/i', $route)) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: home.php
Line: 44:44

Comment:
**logic:** Regex pattern only matches `.inc.php` but original change description mentions blocking `.inc` files too. Should use `/\.inc(?:\.php)?$/i` to match both patterns consistently

```suggestion
    if (preg_match('/\.inc(?:\.php)?$/i', $route)) {
```

How can I resolve this? If you propose a fix, please make it concise.

*/
public static function getDebugMessage(string $defaultMessage, string $debugMessage): string
{
return getenv('OPENEMR_FC_LOG_LEVEL') === 'debug' ? $debugMessage : $defaultMessage;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Environment variable name has extra underscore - should be OPENEMR_FC_LOG_LEVEL not OPENEMR__FC_LOG_LEVEL to match the pattern used elsewhere

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/OpenCoreEMR/FrontController/SecurityValidator.php
Line: 51:51

Comment:
**logic:** Environment variable name has extra underscore - should be `OPENEMR_FC_LOG_LEVEL` not `OPENEMR__FC_LOG_LEVEL` to match the pattern used elsewhere

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +29 to +31
$this->baseDir = realpath($baseDir);
// Ensure baseDir is valid
if ($this->baseDir === false || $this->baseDir === '') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Missing validation after realpath() call. If baseDir is false, the constructor should throw an exception immediately.

Suggested change
$this->baseDir = realpath($baseDir);
// Ensure baseDir is valid
if ($this->baseDir === false || $this->baseDir === '') {
$this->baseDir = realpath($baseDir);
// Ensure baseDir is valid
if ($this->baseDir === false || $this->baseDir === '') {
throw new \RuntimeException('Invalid base directory provided to Router');
}
$this->config = $config ?? new RouteConfig();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/OpenCoreEMR/FrontController/Router.php
Line: 29:31

Comment:
**logic:** Missing validation after realpath() call. If baseDir is false, the constructor should throw an exception immediately.

```suggestion
        $this->baseDir = realpath($baseDir);
        // Ensure baseDir is valid
        if ($this->baseDir === false || $this->baseDir === '') {
            throw new \RuntimeException('Invalid base directory provided to Router');
        }
        $this->config = $config ?? new RouteConfig();
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +67 to +70
*/
public function handleTrailingSlash(): void
{
// Skip trailing slash redirect for .php files
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: The diff shows query string preservation logic that's missing from the current implementation. This will break redirects with query parameters.

Suggested change
*/
public function handleTrailingSlash(): void
{
// Skip trailing slash redirect for .php files
if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
// Ensure proper relative redirect with query string preservation
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
header('Location: ' . $redirectUrl, true, 301);
exit;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/OpenCoreEMR/FrontController/Router.php
Line: 67:70

Comment:
**logic:** The diff shows query string preservation logic that's missing from the current implementation. This will break redirects with query parameters.

```suggestion
        if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
            // Ensure proper relative redirect with query string preservation
            $queryString = $_SERVER['QUERY_STRING'] ?? '';
            $redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
            header('Location: ' . $redirectUrl, true, 301);
            exit;
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +97 to +98
* Resolve target file path
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Path traversal prevention uses strpos() instead of str_starts_with() as shown in the diff. The str_starts_with() approach with DIRECTORY_SEPARATOR is more secure.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/OpenCoreEMR/FrontController/Router.php
Line: 97:98

Comment:
**logic:** Path traversal prevention uses strpos() instead of str_starts_with() as shown in the diff. The str_starts_with() approach with DIRECTORY_SEPARATOR is more secure.

How can I resolve this? If you propose a fix, please make it concise.

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

♻️ Duplicate comments (2)
tests/scripts/test_performance.sh (2)

50-58: Guard against missing bc.

The script exits with command not found when bc isn’t installed (common in minimal CI images). Add the same availability check you already do for ab.

Apply this diff:

 # Check if Apache Bench (ab) is available
 if ! command -v ab &> /dev/null; then
@@
 fi

+# Check if bc (basic calculator) is available
+if ! command -v bc &> /dev/null; then
+    {
+        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "$RED" "$NC"
+        echo "  macOS: brew install bc"
+        echo "  Linux: sudo apt-get install bc"
+    } | tee -a "$REPORT_FILE"
+    exit 1
+fi
+

102-135: Validate ab output before parsing metrics.

Blindly defaulting to "0" masks real ab failures or format changes, producing misleading numbers in the report. Capture the exit code, ensure the expected sections exist, and only then parse.

Apply this diff:

-    # Run Apache Bench test
-    if ! ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1); then
+    # Run Apache Bench test
+    ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1)
+    ab_status=$?
+
+    if [[ $ab_status -ne 0 ]]; then
         {
             printf "%sERROR: Apache Bench failed for %s%s\n" "$RED" "$url" "$NC"
             echo "Output: $ab_output"
         } | tee -a "$REPORT_FILE"
         return 1
     fi
 
-    # Extract key metrics using awk with safe defaults
-    requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output" || echo "0")
-    time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output" || echo "0")
-    failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output" || echo "0")
+    if ! printf '%s\n' "$ab_output" | grep -q "Requests per second"; then
+        {
+            printf "%sERROR: Apache Bench output missing expected metrics for %s%s\n" "$RED" "$url" "$NC"
+            echo "Output: $ab_output"
+        } | tee -a "$REPORT_FILE"
+        return 1
+    fi
+
+    # Extract key metrics using awk now that output is validated
+    requests_per_sec=$(printf '%s\n' "$ab_output" | awk '/Requests per second/ {print $4}')
+    time_per_request=$(printf '%s\n' "$ab_output" | awk '/Time per request.*mean\)/ {print $4; exit}')
+    failed_requests=$(printf '%s\n' "$ab_output" | awk '/Failed requests/ {print $3}')
 
     # Extract percentiles using awk with safe defaults
-    p50=$(awk '/50%/ {print $2}' <<< "$ab_output" || echo "0")
-    p95=$(awk '/95%/ {print $2}' <<< "$ab_output" || echo "0")
-    p99=$(awk '/99%/ {print $2}' <<< "$ab_output" || echo "0")
+    p50=$(printf '%s\n' "$ab_output" | awk '/50%/ {print $2}')
+    p95=$(printf '%s\n' "$ab_output" | awk '/95%/ {print $2}')
+    p99=$(printf '%s\n' "$ab_output" | awk '/99%/ {print $2}')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90cd613 and 08df75e.

📒 Files selected for processing (3)
  • src/OpenCoreEMR/FrontController/RouteConfig.php (1 hunks)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/OpenCoreEMR/FrontController/RouteConfig.php (2)
src/OpenCoreEMR/FrontController/Router.php (2)
  • __construct (27-35)
  • requiresAdmin (91-94)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (7)
  • addForbiddenPath (27-27)
  • addAdminPath (34-34)
  • addDeprecatedPath (41-41)
  • isForbidden (49-49)
  • requiresAdmin (57-57)
  • isDeprecated (65-65)
  • loadFromFile (73-73)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (50-75)
🪛 GitHub Actions: ShellCheck
tests/scripts/test_performance.sh

[info] 22-22: ShellCheck SC2250: Prefer braces around variable references.


[info] 25-25: ShellCheck SC2292: Prefer [[ ]] over [ ] for tests in Bash/Ksh.


[info] 25-25: ShellCheck SC2250: Prefer braces around variable references.

tests/scripts/test_compatibility.sh

[info] 24-24: ShellCheck SC2292: Prefer [[ ]] over [ ] for tests in Bash/Ksh.


[info] 24-24: ShellCheck SC2250: Prefer braces around variable references.

🪛 GitHub Check: ShellCheck
tests/scripts/test_performance.sh

[warning] 45-45:
Prefer putting braces around variable references even when not strictly required.


[warning] 44-44:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 40-40:
Prefer putting braces around variable references even when not strictly required.


[warning] 25-25:
Prefer putting braces around variable references even when not strictly required.


[warning] 25-25:
Prefer [[ ]] over [ ] for tests in Bash/Ksh.


[warning] 22-22:
Prefer putting braces around variable references even when not strictly required.

tests/scripts/test_compatibility.sh

[warning] 41-41:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 32-32:
YELLOW appears unused. Verify use (or export if used externally).


[warning] 24-24:
Prefer putting braces around variable references even when not strictly required.


[warning] 24-24:
Prefer [[ ]] over [ ] for tests in Bash/Ksh.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: phpstan (8.4)

Comment on lines +184 to +204
$config = require $realPath;

if (isset($config['forbidden']) && is_array($config['forbidden'])) {
foreach ($config['forbidden'] as $pattern) {
$this->addForbiddenPath($pattern);
}
}

if (isset($config['admin']) && is_array($config['admin'])) {
foreach ($config['admin'] as $pattern) {
$this->addAdminPath($pattern);
}
}

if (isset($config['deprecated']) && is_array($config['deprecated'])) {
foreach ($config['deprecated'] as $pattern) {
$this->addDeprecatedPath($pattern);
}
}

return true;
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

Guard against non-array configs before accessing offsets

If the required config file returns null, bool, or any non-array value (syntax error, empty include, etc.), the subsequent isset($config['…']) calls will raise “Trying to access array offset on… ” warnings/fatal errors. Add an is_array guard and fail fast to keep loadFromFile reliable.

-        $config = require $realPath;
+        $config = require $realPath;
+
+        if (!is_array($config)) {
+            return false;
+        }
📝 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
$config = require $realPath;
if (isset($config['forbidden']) && is_array($config['forbidden'])) {
foreach ($config['forbidden'] as $pattern) {
$this->addForbiddenPath($pattern);
}
}
if (isset($config['admin']) && is_array($config['admin'])) {
foreach ($config['admin'] as $pattern) {
$this->addAdminPath($pattern);
}
}
if (isset($config['deprecated']) && is_array($config['deprecated'])) {
foreach ($config['deprecated'] as $pattern) {
$this->addDeprecatedPath($pattern);
}
}
return true;
$config = require $realPath;
if (!is_array($config)) {
return false;
}
if (isset($config['forbidden']) && is_array($config['forbidden'])) {
foreach ($config['forbidden'] as $pattern) {
$this->addForbiddenPath($pattern);
}
}
if (isset($config['admin']) && is_array($config['admin'])) {
foreach ($config['admin'] as $pattern) {
$this->addAdminPath($pattern);
}
}
if (isset($config['deprecated']) && is_array($config['deprecated'])) {
foreach ($config['deprecated'] as $pattern) {
$this->addDeprecatedPath($pattern);
}
}
return true;
🤖 Prompt for AI Agents
In src/OpenCoreEMR/FrontController/RouteConfig.php around lines 184 to 204, the
code assumes the required file returns an array and directly accesses offsets
which can trigger "Trying to access array offset on..." warnings if the require
returns null/false/non-array; add an immediate is_array($config) guard after
$config = require $realPath and fail fast (return false or throw a descriptive
exception) when it's not an array so the subsequent isset/access code only runs
on valid array configs.

Comment on lines +232 to +240
if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$((FAILED_TESTS + 1))
echo " ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
else
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$((PASSED_TESTS + 1))
echo " ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
fi
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

Fail POST compatibility tests on 5xx/connection errors.

Any 5xx (or 000 curl error) currently lands in the “PASS” branch, so a broken login POST can still count as success. Tighten the check so unexpected 3‑digit codes fail the test.

Apply this diff:

-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
+if [[ "${http_code}" = "404" || "${http_code}" = "403" || "${http_code}" = "000" || "${http_code}" =~ ^5 ]]; then
     printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
     FAILED_TESTS=$((FAILED_TESTS + 1))
     echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
 else
📝 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
if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$((FAILED_TESTS + 1))
echo " ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
else
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$((PASSED_TESTS + 1))
echo " ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
fi
if [[ "${http_code}" = "404" || "${http_code}" = "403" || "${http_code}" = "000" || "${http_code}" =~ ^5 ]]; then
printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$((FAILED_TESTS + 1))
echo " ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
else
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$((PASSED_TESTS + 1))
echo " ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
fi
🤖 Prompt for AI Agents
In tests/scripts/test_compatibility.sh around lines 232 to 240, the current
logic treats any HTTP response other than 403/404 as a PASS (so 5xx or curl
"000" succeed); change the branching so only 2xx responses are considered PASS
and everything else (including 5xx and "000") is treated as FAIL. Concretely,
replace the condition with a check that http_code starts with "2" (e.g. using
shell pattern or regex) to mark PASS and otherwise increment FAILED_TESTS and
print the FAIL messages; ensure the PASS branch increments PASSED_TESTS and
prints the success message as before.

@coding-sunshine coding-sunshine force-pushed the 8964-global-front-controller branch from 08df75e to 90cd613 Compare October 4, 2025 08:04
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Summary

**Note: This review covers only the changes made since the last review, not the entire PR.**

This incremental update addresses specific reliability and robustness issues in the OpenEMR front controller testing infrastructure. The changes focus on improving the compatibility and performance test scripts to work correctly across different execution environments, particularly non-interactive ones like CI/CD pipelines.

The key improvements include:

  1. Enhanced Environment Compatibility: Both test_compatibility.sh and test_performance.sh now include fallback logic for color output handling. The scripts check for tput availability and the TERM environment variable before attempting to set color codes, preventing script failures in headless or automated environments.

  2. Critical Bug Fix in Route Configuration: The RouteConfig.php file addresses a fatal error where the non-existent array_any() function was replaced with a proper manual foreach loop implementation. Additionally, method names were improved from "Pattern" to "Path" for better semantic clarity (addForbiddenPatternaddForbiddenPath, etc.).

These changes ensure the front controller testing suite can run reliably in automated testing environments while maintaining colored output when available in interactive terminals. The route configuration fix prevents runtime fatal errors and provides clearer method naming that better reflects the functionality of adding path-based routing rules.

Important Files Changed

Changed Files
Filename Score Overview
tests/scripts/test_compatibility.sh 4/5 Added fallback color handling for non-interactive environments to prevent script failures in CI/CD
src/OpenCoreEMR/FrontController/RouteConfig.php 4/5 Fixed critical bug replacing non-existent array_any() function and renamed methods for clarity
tests/scripts/test_performance.sh 4/5 Added color output fallbacks for headless environments to improve script portability

Confidence score: 4/5

  • This PR addresses important reliability issues in the testing infrastructure with minimal risk of breaking existing functionality
  • Score reflects well-targeted fixes for specific environmental compatibility issues and a critical bug fix
  • All changes are focused on testing scripts and internal routing logic that don't affect end-user functionality

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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: 0

♻️ Duplicate comments (8)
tests/scripts/test_performance.sh (3)

232-252: Regex doesn't handle negative numbers in performance measurements.

Line 234's regex ^[0-9]+\.?[0-9]*$ rejects negative values, which could occur in performance metrics (e.g., negative overhead if front controller is faster than baseline).

Update the regex to handle negative numbers:

-    if ! [[ "$baseline_time" =~ ^[0-9]+\.?[0-9]*$ ]] || ! [[ "$fc_time" =~ ^[0-9]+\.?[0-9]*$ ]]; then
+    if ! [[ "$baseline_time" =~ ^-?[0-9]+\.?[0-9]*$ ]] || ! [[ "$fc_time" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then

Also update line 307 for consistency:

-if [[ -n "$overhead" ]] && [[ "$overhead" =~ ^[0-9]+\.?[0-9]*$ ]]; then
+if [[ -n "$overhead" ]] && [[ "$overhead" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then

Based on past review comments.


48-56: Add dependency check for bc command.

The script uses bc for floating-point arithmetic comparisons (lines 63, 68, 73, 239, 250, 308) but doesn't verify its availability. Missing bc will cause runtime failures.

Apply this diff to add the check before the ab check:

+# Check if bc (basic calculator) is available for floating point arithmetic
+if ! command -v bc &> /dev/null; then
+    {
+        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "$RED" "$NC"
+        echo "  macOS: brew install bc"
+        echo "  Linux: sudo apt-get install bc"
+    } | tee -a "$REPORT_FILE"
+    exit 1
+fi
+
 # Check if Apache Bench (ab) is available

Based on past review comments.


100-133: Validate ab exit code and output format before parsing metrics.

Current implementation uses || echo "0" fallbacks that mask ab failures and parsing errors, potentially reporting misleading zero values for legitimate errors (network failures, ab crashes, malformed output).

Replace the metric extraction logic with validated parsing:

-    # Run Apache Bench test
-    if ! ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1); then
+    # Run ab and capture output + exit code
+    set +e
+    ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1)
+    ab_exit_code=$?
+    set -e
+
+    # Verify ab succeeded and output contains expected markers
+    if [ "$ab_exit_code" -ne 0 ] || ! printf '%s\n' "$ab_output" | grep -q "Requests per second"; then
         {
-            printf "%sERROR: Apache Bench failed for %s%s\n" "$RED" "$url" "$NC"
-            echo "Output: $ab_output"
+            printf "%sERROR: ab failed (exit code %s) or unexpected output format for %s%s\n" "$RED" "$ab_exit_code" "$url" "$NC"
         } | tee -a "$REPORT_FILE"
         return 1
     fi
 
-    # Extract key metrics using awk with safe defaults
-    requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output" || echo "0")
-    time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output" || echo "0")
-    failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output" || echo "0")
+    # Extract key metrics using printf + pipe (POSIX-compatible, no here-strings)
+    requests_per_sec=$(printf '%s\n' "$ab_output" | awk '/Requests per second/ {print $4}')
+    time_per_request=$(printf '%s\n' "$ab_output" | awk '/Time per request.*mean\)/ {print $4; exit}')
+    failed_requests=$(printf '%s\n' "$ab_output" | awk '/Failed requests/ {print $3}')
 
-    # Extract percentiles using awk with safe defaults
-    p50=$(awk '/50%/ {print $2}' <<< "$ab_output" || echo "0")
-    p95=$(awk '/95%/ {print $2}' <<< "$ab_output" || echo "0")
-    p99=$(awk '/99%/ {print $2}' <<< "$ab_output" || echo "0")
+    # Extract percentiles
+    p50=$(printf '%s\n' "$ab_output" | awk '/50%/ {print $2}')
+    p95=$(printf '%s\n' "$ab_output" | awk '/95%/ {print $2}')
+    p99=$(printf '%s\n' "$ab_output" | awk '/99%/ {print $2}')

This ensures:

  • ab execution failures are detected
  • Missing sections in output trigger warnings
  • Legitimate zero values (e.g., no failed requests) are preserved
  • POSIX-compatible (no bash-specific here-strings)

Based on past review comments.

tests/Tests/FrontController/CompatibilityTest.php (4)

20-22: Remove unused import.

RequestException is imported but never used in the test suite.

 use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
 use PHPUnit\Framework\TestCase;

Based on past review comments.


193-197: Use assertContains correctly for arrays.

In PHPUnit 9+, assertContains is for arrays. For string searches, use assertStringContainsString. Since you're checking if a value is in an array of status codes, assertContains is correct here, but ensure you're using a compatible PHPUnit version.

Verify your PHPUnit version supports this assertion:

#!/bin/bash
# Check PHPUnit version in composer.json and composer.lock

echo "=== composer.json ==="
if [ -f composer.json ]; then
  jq -r '.require["phpunit/phpunit"] // .["require-dev"]["phpunit/phpunit"] // "not found"' composer.json
fi

echo -e "\n=== composer.lock (installed version) ==="
if [ -f composer.lock ]; then
  jq -r '.packages[] | select(.name == "phpunit/phpunit") | .version' composer.lock
fi

echo -e "\n=== Recommendation ==="
echo "PHPUnit 9+ is recommended (PHPUnit 2.x is obsolete)."
echo "If using PHPUnit 9+, assertContains for arrays is correct."
echo "If seeing deprecation warnings, update PHPUnit or use assertContains() explicitly."

Based on past review comments and learnings about PHPUnit versions.


254-258: Same issue: verify assertContains usage.

See comment on lines 193-197. Apply the same verification.

Based on past review comments.


273-277: Same issue: verify assertContains usage.

See comment on lines 193-197. Apply the same verification.

Based on past review comments.

tests/scripts/test_compatibility.sh (1)

223-239: POST compatibility test has two issues.

  1. Inconsistent test counting: Manually incrementing TOTAL_TESTS instead of using the run_test function breaks the established pattern (lines 85-90, 91-95, etc.).

  2. Weak error detection: The test passes for 5xx server errors and curl connection failures (000), masking broken POST handling.

Refactor to use run_test or tighten the logic:

Option 1 (preferred): Use run_test function

-TOTAL_TESTS=$((TOTAL_TESTS + 1))
-printf "Testing: POST request handling... " | tee -a "${REPORT_FILE}"
-
-http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-    -d "authUser=test&authPass=test" \
-    "${BASE_URL}/interface/login/login.php")
-
-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
-    printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
-    FAILED_TESTS=$((FAILED_TESTS + 1))
-    echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
-else
-    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
-    PASSED_TESTS=$((PASSED_TESTS + 1))
-    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
-fi
-echo | tee -a "${REPORT_FILE}"
+# Note: run_test doesn't support POST, so we keep manual test but fix the logic
+TOTAL_TESTS=$((TOTAL_TESTS + 1))
+printf "Testing: POST request handling... " | tee -a "${REPORT_FILE}"
+
+http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+    -d "authUser=test&authPass=test" \
+    "${BASE_URL}/interface/login/login.php")
+
+# Only 2xx responses are considered success
+if [[ "${http_code}" =~ ^2 ]]; then
+    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
+    PASSED_TESTS=$((PASSED_TESTS + 1))
+    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
+else
+    printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
+    FAILED_TESTS=$((FAILED_TESTS + 1))
+    echo "  ✗ POST requests failed or blocked (expected 2xx)" | tee -a "${REPORT_FILE}"
+fi
+echo | tee -a "${REPORT_FILE}"

Based on past review comments.

🧹 Nitpick comments (4)
index.php (1)

8-11: LGTM! Security headers improve defense-in-depth.

The addition of X-Content-Type-Options, X-Frame-Options, and X-XSS-Protection headers early in the request lifecycle strengthens security posture against MIME-sniffing, clickjacking, and XSS attacks.

Consider adding a Content-Security-Policy header for additional protection:

 header('X-Content-Type-Options: nosniff');
 header('X-Frame-Options: SAMEORIGIN');
 header('X-XSS-Protection: 1; mode=block');
+header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");

Note: Adjust CSP directives based on your actual resource requirements. Start with a restrictive policy and relax as needed.

tests/scripts/test_performance.sh (1)

16-56: Consider adding braces around variable references.

ShellCheck flags multiple instances where variable references could have braces for consistency and safety (lines 22, 38, 42-44, 46, 51, 54).

While not strictly required for these cases, adding braces improves consistency:

-REPORT_FILE="$REPORT_DIR/performance-test-report-$(date +%Y%m%d-%H%M%S).txt"
+REPORT_FILE="${REPORT_DIR}/performance-test-report-$(date +%Y%m%d-%H%M%S).txt"

Apply similar changes to other flagged lines if you prefer consistent style throughout the script.

tests/scripts/test_compatibility.sh (1)

37-42: Consider separating command invocation to avoid masking return value.

ShellCheck SC2312 warns that the date command in the pipeline could fail silently. While unlikely, separating the command improves error detection.

Apply this diff:

+date_output=$(date)
 {
     echo "Compatibility Test Report"
-    echo "Date: $(date)"
+    echo "Date: ${date_output}"
     echo "URL: ${BASE_URL}"
     echo
 } | tee "${REPORT_FILE}"

Or add || true if you want to explicitly ignore date failures:

 {
     echo "Compatibility Test Report"
-    echo "Date: $(date)"
+    echo "Date: $(date || true)"
     echo "URL: ${BASE_URL}"
     echo
 } | tee "${REPORT_FILE}"
home.php (1)

66-82: Consider extracting pass-through logic into a function.

The pass-through block (lines 35-82) is a substantial chunk of code that could benefit from extraction into a named function or separate file for improved maintainability and testability.

Example refactor:

+/**
+ * Execute pass-through mode (feature flag disabled)
+ * @return never
+ */
+function executePassThrough(): void {
+    // Extract target route from query parameter
+    $route = $_GET['_ROUTE'] ?? 'index.php';
+
+    // Security: Prevent directory traversal and null byte injection
+    if (strpos($route, '..') !== false || strpos($route, "\0") !== false) {
+        http_response_code(404);
+        exit('Not Found');
+    }
+
+    // ... rest of pass-through logic ...
+    
+    // Include target file - it now executes with correct context
+    require $targetFile;
+    exit;
+}
+
 // Check feature flag - when disabled, provide transparent pass-through
 if ((getenv('OPENEMR_ENABLE_FRONT_CONTROLLER') ?: '0') !== '1') {
-    // Extract target route from query parameter
-    $route = $_GET['_ROUTE'] ?? 'index.php';
-    // ... rest of pass-through logic ...
-    require $targetFile;
-    exit;
+    executePassThrough();
 }

This would:

  • Improve code organization
  • Make the pass-through logic easier to test
  • Reduce cognitive load in the main flow
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea0ed7f and 54db214.

📒 Files selected for processing (7)
  • home.php (1 hunks)
  • index.php (1 hunks)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Tests/FrontController/SecurityTest.php
  • tests/scripts/test_security.sh
🧰 Additional context used
🧬 Code graph analysis (3)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (48-73)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-39)
home.php (3)
src/OpenCoreEMR/FrontController/Router.php (8)
  • Router (20-136)
  • determineSiteId (54-61)
  • extractRoute (40-49)
  • handleTrailingSlash (68-78)
  • isForbiddenPath (83-86)
  • requiresAdmin (91-94)
  • resolveTargetFile (99-119)
  • getSiteId (132-135)
src/OpenCoreEMR/FrontController/SecurityValidator.php (4)
  • SecurityValidator (20-53)
  • logSecurityEvent (41-44)
  • isIncludeFile (33-36)
  • getDebugMessage (49-52)
src/Events/FrontController/FrontControllerEvent.php (2)
  • FrontControllerEvent (23-136)
  • getSiteId (86-89)
🪛 GitHub Actions: ShellCheck
tests/scripts/test_compatibility.sh

[info] 39-39: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_performance.sh

[info] 22-22: ShellCheck SC2250: Prefer putting braces around variable references even when not strictly required.


[info] 38-38: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

🪛 GitHub Check: ShellCheck
tests/scripts/test_compatibility.sh

[warning] 39-39:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_performance.sh

[warning] 54-54:
Prefer putting braces around variable references even when not strictly required.


[warning] 51-51:
Prefer putting braces around variable references even when not strictly required.


[warning] 51-51:
Prefer putting braces around variable references even when not strictly required.


[warning] 46-46:
Prefer putting braces around variable references even when not strictly required.


[warning] 44-44:
Prefer putting braces around variable references even when not strictly required.


[warning] 43-43:
Prefer putting braces around variable references even when not strictly required.


[warning] 42-42:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 38-38:
Prefer putting braces around variable references even when not strictly required.


[warning] 22-22:
Prefer putting braces around variable references even when not strictly required.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: Rector PHP Analysis (8.4)
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
🔇 Additional comments (8)
home.php (6)

25-28: LGTM! Security headers are set consistently.

The security headers match those added to index.php, providing consistent protection across entry points.


35-82: Pass-through mode provides good fallback behavior.

The transparent pass-through when the feature flag is disabled ensures backward compatibility while the front controller is being adopted. The security checks (path traversal, .inc blocking, file validation) provide essential protection even in pass-through mode.


84-90: Event dispatching enables extensibility.

The early event hook allows modules to intercept requests before routing, which is useful for authentication, rate limiting, or custom logging. Good integration with the existing kernel event system.


92-133: Router integration is clean and secure.

The router properly:

  • Determines site ID for multisite support
  • Extracts and validates routes
  • Handles trailing slashes
  • Checks forbidden/admin paths
  • Blocks .inc files
  • Resolves target files with path traversal protection

135-166: Error handling and late events provide robust lifecycle management.

The shutdown function ensures late events fire even if the target exits early, and the try/catch block properly logs and re-throws exceptions while still dispatching events. This provides good observability for monitoring and debugging.


30-33: Dotenv configuration verified: .env.example includes OPENEMR_ENABLE_FRONT_CONTROLLER and front-controller docs reference the flag; safeLoad()’s silent fallback is acceptable.

tests/Tests/FrontController/CompatibilityTest.php (2)

29-37: Test setup looks good.

The setUpBeforeClass method properly initializes the Guzzle client with sensible defaults (disable errors, allow redirects). The base URL is configurable via environment variable with a sensible default.


39-300: Comprehensive test coverage validates compatibility.

The test suite covers all critical paths:

  • Core entry points (index, login, setup)
  • Existing front controllers (REST API, portal, OAuth2)
  • Multisite support
  • Core workflows (patient file, calendar, billing)
  • Static assets
  • Custom modules
  • POST requests
  • File uploads
  • Query string preservation
  • Report generation

The assertions appropriately handle multiple valid response codes (200, 302, 401, etc.) and validate content types for static assets.

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

♻️ Duplicate comments (3)
tests/scripts/test_performance.sh (3)

58-83: Check for bc before using it.

bc is required in evaluate_overhead, but on many minimal images it isn’t installed, so the script dies the first time it hits those comparisons. Please add a dependency check (just like you do for ab) so the script fails fast with a friendly message.

-# Check if Apache Bench (ab) is available
-if ! command -v ab &> /dev/null; then
+# Check if bc (basic calculator) is available
+if ! command -v bc &> /dev/null; then
+    {
+        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "${RED}" "${NC}"
+        echo "  macOS: brew install bc"
+        echo "  Linux: sudo apt-get install bc"
+    } | tee -a "${REPORT_FILE}"
+    exit 1
+fi
+
+# Check if Apache Bench (ab) is available
+if ! command -v ab &> /dev/null; then

109-118: Validate ab output before parsing metrics.

This still pipes raw output through awk <<< … || echo "0", so any parsing failure silently becomes zero and hides real issues (exactly the scenario flagged in the earlier review). Please gate the parsing on ab exit code and expected markers, then parse via printf '%s\n' "$ab_output" | awk … so legitimate zeros stay intact and malformed output trips an error.


307-323: Allow negative overhead values.

When the front controller beats the baseline, overhead becomes negative (e.g., -3.25), but the regex ^[0-9]+\.?[0-9]*$ rejects it, so you skip recommendations entirely. Let the pattern accept an optional leading minus (and perhaps more flexible decimal formats) before testing thresholds.

-if [[ -n "${overhead}" ]] && [[ "${overhead}" =~ ^[0-9]+\.?[0-9]*$ ]]; then
+if [[ -n "${overhead}" ]] && [[ "${overhead}" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6c0f40 and c7219c9.

📒 Files selected for processing (2)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (50-75)
🪛 GitHub Actions: ShellCheck
tests/scripts/test_performance.sh

[info] 42-42: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 63-63: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 68-68: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 73-73: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 143-143: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[info] 160-160: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[info] 177-177: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[warning] 183-183: ShellCheck SC2034: login_time appears unused. Verify use (or export if used externally).


[info] 195-195: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[info] 216-216: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[info] 262-262: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 279-279: ShellCheck SC2009: Consider using pgrep instead of grepping ps output.


[info] 279-279: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 287-287: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 289-289: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 289-289: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 292-292: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 292-292: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 308-308: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

🪛 GitHub Check: ShellCheck
tests/scripts/test_performance.sh

[warning] 195-195:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.


[warning] 183-183:
login_time appears unused. Verify use (or export if used externally).


[warning] 177-177:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.


[warning] 160-160:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.


[warning] 143-143:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.


[warning] 73-73:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 68-68:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 63-63:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 42-42:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests

Comment on lines +143 to +168
baseline=$(run_performance_test \
"Direct index.php access" \
"${BASE_URL}/index.php" \
"Direct file access without front controller")

baseline_rps=$(cut -d'|' -f1 <<< "${baseline}")
baseline_time=$(cut -d'|' -f2 <<< "${baseline}")
baseline_p95=$(cut -d'|' -f3 <<< "${baseline}")

# Test 2: Front Controller Routing
{
echo "========================================"
echo "FRONT CONTROLLER: Routed Access"
echo "========================================"
echo
} | tee -a "${REPORT_FILE}"

fc_result=$(run_performance_test \
"Front controller routing" \
"${BASE_URL}/home.php?_ROUTE=index.php" \
"Access through front controller")

fc_rps=$(cut -d'|' -f1 <<< "${fc_result}")
fc_time=$(cut -d'|' -f2 <<< "${fc_result}")
fc_p95=$(cut -d'|' -f3 <<< "${fc_result}")

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

Handle failed performance runs before using their metrics.

run_performance_test returns 1 when ab blows up, yet these command substitutions keep the script running with unset metrics (because set -e is disabled inside $(...)). The analysis phase then divides empty strings, so the report shows garbage instead of failing fast. Guard each assignment with an if ! …; then exit 1; fi (or similar) so a broken test aborts before producing bogus numbers.

-if ! ab_output=$(ab -n "${ITERATIONS}" -c 10 -q "${url}" 2>&1); then
+if ! ab_output=$(ab -n "${ITERATIONS}" -c 10 -q "${url}" 2>&1); then
@@
-baseline=$(run_performance_test \
-    "Direct index.php access" \
-    "${BASE_URL}/index.php" \
-    "Direct file access without front controller")
+if ! baseline=$(run_performance_test \
+    "Direct index.php access" \
+    "${BASE_URL}/index.php" \
+    "Direct file access without front controller"); then
+    exit 1
+fi
@@
-fc_result=$(run_performance_test \
-    "Front controller routing" \
-    "${BASE_URL}/home.php?_ROUTE=index.php" \
-    "Access through front controller")
+if ! fc_result=$(run_performance_test \
+    "Front controller routing" \
+    "${BASE_URL}/home.php?_ROUTE=index.php" \
+    "Access through front controller"); then
+    exit 1
+fi

(Apply the same pattern to the other run_performance_test assignments.)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: ShellCheck

[info] 143-143: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.


[info] 160-160: ShellCheck SC2311: Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e before it or enable inherit_errexit.

🪛 GitHub Check: ShellCheck

[warning] 160-160:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.


[warning] 143-143:
Bash implicitly disabled set -e for this function invocation because it's inside a command substitution. Add set -e; before it or enable inherit_errexit.

🤖 Prompt for AI Agents
In tests/scripts/test_performance.sh around lines 143 to 168, the script assigns
results from run_performance_test into variables but doesn't check for failure,
so a failed run returns non-zero/empty output and later metric parsing produces
garbage; update each assignment to first capture the command exit status (e.g.,
run the command and test its exit code or use if ! result=$(run_performance_test
...); then echo "Performance test failed" >&2; exit 1; fi) and only parse
metrics when the run succeeded, and apply the same guard pattern to all other
run_performance_test calls in the file.

Comment on lines +23 to +31
if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
NC=""
fi
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

Prevent tput from aborting under set -e.

In CI we often get TERM=dumb (or a terminfo without color support). In that case each tput setaf … returns exit code 1, and because the script runs with set -e, the entire test suite exits before executing any checks. Please swallow those failures and fall back to plain text.

-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
-else
-    RED=""
-    GREEN=""
-    NC=""
-fi
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || true)
+    GREEN=$(tput setaf 2 2>/dev/null || true)
+    NC=$(tput sgr0 2>/dev/null || true)
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
+else
+    RED=""; GREEN=""; NC=""
+fi
📝 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
if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
NC=""
fi
if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
RED=$(tput setaf 1 2>/dev/null || true)
GREEN=$(tput setaf 2 2>/dev/null || true)
NC=$(tput sgr0 2>/dev/null || true)
if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
RED=""; GREEN=""; NC=""
fi
else
RED=""; GREEN=""; NC=""
fi
🤖 Prompt for AI Agents
In tests/scripts/test_security.sh around lines 23 to 31, tput calls can return
non-zero under CI (TERM=dumb) and cause the script to abort with set -e; modify
the assignments to tolerate failures by redirecting tput stderr and ensuring
non-zero exits are ignored (e.g., run each tput with stderr redirected and use
"|| true" or capture output into variables only if tput succeeds), then fall
back to empty strings when tput fails so colors are optional and the script
never exits due to tput errors.

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

♻️ Duplicate comments (7)
tests/scripts/test_performance.sh (4)

65-92: Add a guard for the bc dependency before calling it

bc is invoked here without first confirming it exists, so the script will die with “command not found” on environments lacking bc (common on fresh macOS/Linux installs). Please fail fast with a dependency check the same way you already do for ab.

+# Check if bc (basic calculator) is available
+if ! command -v bc >/dev/null 2>&1; then
+    {
+        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "${RED}" "${NC}"
+        echo "  macOS: brew install bc"
+        echo "  Linux: sudo apt-get install bc"
+    } | tee -a "${REPORT_FILE}"
+    exit 1
+fi
+

118-126: Validate ab output before parsing metrics

These here-strings plus || echo "0" still mask malformed ab output. If ab prints an error while exiting 0, every metric silently becomes 0, so your report claims success instead of failing fast. Capture the exit code and assert the expected markers exist before you parse.

-    requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "${ab_output}" || echo "0")
-    time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "${ab_output}" || echo "0")
-    failed_requests=$(awk '/Failed requests/ {print $3}' <<< "${ab_output}" || echo "0")
-
-    # Extract percentiles using awk with safe defaults
-    p50=$(awk '/50%/ {print $2}' <<< "${ab_output}" || echo "0")
-    p95=$(awk '/95%/ {print $2}' <<< "${ab_output}" || echo "0")
-    p99=$(awk '/99%/ {print $2}' <<< "${ab_output}" || echo "0")
+    ab_exit_code=0
+    requests_marker=$(printf '%s\n' "${ab_output}" | grep -c "Requests per second" || ab_exit_code=$?)
+    if [[ "${ab_exit_code}" -ne 0 || "${requests_marker}" -eq 0 ]]; then
+        {
+            printf "%sWARNING: ab output missing expected markers for %s%s\n" "${RED}" "${url}" "${NC}"
+        } | tee -a "${REPORT_FILE}"
+        return 1
+    fi
+
+    requests_per_sec=$(printf '%s\n' "${ab_output}" | awk '/Requests per second/ {print $4}')
+    time_per_request=$(printf '%s\n' "${ab_output}" | awk '/Time per request.*mean\)/ {print $4; exit}')
+    failed_requests=$(printf '%s\n' "${ab_output}" | awk '/Failed requests/ {print $3}')
+
+    p50=$(printf '%s\n' "${ab_output}" | awk '/50%/ {print $2}')
+    p95=$(printf '%s\n' "${ab_output}" | awk '/95%/ {print $2}')
+    p99=$(printf '%s\n' "${ab_output}" | awk '/99%/ {print $2}')

152-231: Stop on failed run_performance_test executions

run_performance_test returns 1 when ab fails, but because these calls sit inside command substitutions, set -e is suppressed and the script keeps going with empty metrics. Later arithmetic then manipulates blank strings. Wrap each call in an if ! …; then exit 1; fi guard so a busted run aborts instead of producing nonsense.

-if ! ab_output=$(ab -n "${ITERATIONS}" -c 10 -q "${url}" 2>&1); then
+if ! ab_output=$(ab -n "${ITERATIONS}" -c 10 -q "${url}" 2>&1); then
@@
-baseline=$(run_performance_test \
-    "Direct index.php access" \
-    "${BASE_URL}/index.php" \
-    "Direct file access without front controller")
+if ! baseline=$(run_performance_test \
+    "Direct index.php access" \
+    "${BASE_URL}/index.php" \
+    "Direct file access without front controller"); then
+    exit 1
+fi
@@
-fc_result=$(run_performance_test \
-    "Front controller routing" \
-    "${BASE_URL}/home.php?_ROUTE=index.php" \
-    "Access through front controller")
+if ! fc_result=$(run_performance_test \
+    "Front controller routing" \
+    "${BASE_URL}/home.php?_ROUTE=index.php" \
+    "Access through front controller"); then
+    exit 1
+fi
@@
-login_result=$(run_performance_test \
-    "Login page access" \
-    "${BASE_URL}/interface/login/login.php" \
-    "Anonymous page with ignoreAuth pattern")
+if ! login_result=$(run_performance_test \
+    "Login page access" \
+    "${BASE_URL}/interface/login/login.php" \
+    "Anonymous page with ignoreAuth pattern"); then
+    exit 1
+fi
@@
-    static_result=$(run_performance_test \
-        "Static CSS file" \
-        "${BASE_URL}/public/assets/css/style.css" \
-        "Static assets should bypass front controller")
+    if ! static_result=$(run_performance_test \
+        "Static CSS file" \
+        "${BASE_URL}/public/assets/css/style.css" \
+        "Static assets should bypass front controller"); then
+        exit 1
+    fi
@@
-api_result=$(run_performance_test \
-    "REST API endpoint" \
-    "${BASE_URL}/apis/default/api/patient" \
-    "Existing API front controller (should not change)")
+if ! api_result=$(run_performance_test \
+    "REST API endpoint" \
+    "${BASE_URL}/apis/default/api/patient" \
+    "Existing API front controller (should not change)"); then
+    exit 1
+fi

317-333: Allow negative overhead values when front controller is faster

A faster front controller yields a negative overhead, but the current [[ "${overhead}" =~ ^[0-9]+\.?[0-9]*$ ]] check rejects the minus sign and drops into “Unable to calculate overhead.” Accept an optional leading - (and matching decimal forms) so improvements are reported instead of discarded.

-if [[ -n "${overhead}" ]] && [[ "${overhead}" =~ ^[0-9]+\.?[0-9]*$ ]]; then
+if [[ -n "${overhead}" ]] && [[ "${overhead}" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
tests/scripts/test_compatibility.sh (1)

225-240: POST requests must fail on 5xx/connection errors.

Reiterating prior feedback: anything other than a 2xx (including 5xx or curl “000”) currently reports PASS. Tighten the check so only 2xx pass and everything else fails.

-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
-    printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
-    FAILED_TESTS=$(( FAILED_TESTS + 1 ))
-    echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
-else
-    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
-    PASSED_TESTS=$(( PASSED_TESTS + 1 ))
-    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
-fi
+if [[ "${http_code}" =~ ^2[0-9][0-9]$ ]]; then
+    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
+    PASSED_TESTS=$(( PASSED_TESTS + 1 ))
+    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
+else
+    printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
+    FAILED_TESTS=$(( FAILED_TESTS + 1 ))
+    if [[ "${http_code}" = "000" ]]; then
+        echo "  ✗ POST request failed to reach the server" | tee -a "${REPORT_FILE}"
+    else
+        echo "  ✗ POST requests are being blocked or failing" | tee -a "${REPORT_FILE}"
+    fi
+fi
tests/scripts/test_security.sh (2)

15-18: Propagate pipeline failures with set -o pipefail.

Previous feedback still applies: without set -o pipefail, errors before | tee are swallowed. Keep CI honest by enabling pipefail right after set -e.

 set -e
+# Ensure pipeline failures bubble up (addresses ShellCheck SC2312)
+set -o pipefail
 # Ensure errexit is inherited in command substitutions (addresses SC2311)
 shopt -s inherit_errexit 2>/dev/null || true

25-33: Stop tput from killing the script.

Reiterating: under set -e, a failing tput exits the suite (TERM=dumb, missing terminfo, etc.). Guard the calls and fall back to plain output.

-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
-else
-    RED=""
-    GREEN=""
-    NC=""
-fi
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || printf '')
+    GREEN=$(tput setaf 2 2>/dev/null || printf '')
+    NC=$(tput sgr0 2>/dev/null || printf '')
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
+else
+    RED=""; GREEN=""; NC=""
+fi
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2a9efb and 7bbd462.

📒 Files selected for processing (3)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (52-77)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (50-75)
🪛 GitHub Actions: ShellCheck
tests/scripts/test_security.sh

[info] 40-40: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_compatibility.sh

[info] 41-41: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_performance.sh

[info] 44-44: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 271-271: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 297-297: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 299-299: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[info] 302-302: ShellCheck SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

🪛 GitHub Check: ShellCheck
tests/scripts/test_security.sh

[warning] 40-40:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_compatibility.sh

[warning] 41-41:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

tests/scripts/test_performance.sh

[warning] 302-302:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 302-302:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 299-299:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 299-299:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 297-297:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 271-271:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).


[warning] 44-44:
Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests

Comment on lines +25 to +33
# Colors for output using tput (with fallback for non-interactive environments)
if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NC=$(tput sgr0) # No Color
else
RED=""
GREEN=""
NC=""
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

Let tput failures fall back instead of aborting.

With set -e, any non-zero tput (common when TERM lacks color support) terminates the suite before tests run. Swallow those failures and fall back to plain text.

-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
-else
-    RED=""
-    GREEN=""
-    NC=""
-fi
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || printf '')
+    GREEN=$(tput setaf 2 2>/dev/null || printf '')
+    NC=$(tput sgr0 2>/dev/null || printf '')
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
+else
+    RED=""; GREEN=""; NC=""
+fi
🤖 Prompt for AI Agents
In tests/scripts/test_compatibility.sh around lines 25 to 33, tput failures
currently can produce non-zero exits under set -e and abort the test suite;
change the assignments to swallow tput errors by redirecting stderr and making
the command non-fatal (e.g., capture tput output with 2>/dev/null and fall back
on an empty string on failure using || true or || :), so RED, GREEN, and NC are
set to color values when available but quietly fall back to empty strings
without causing the script to exit.

@coding-sunshine coding-sunshine force-pushed the 8964-global-front-controller branch from cfb7ea2 to 5b45567 Compare October 6, 2025 08:55
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

♻️ Duplicate comments (2)
tests/scripts/test_compatibility.sh (2)

25-34: Keep going when tput is unavailable.

With set -e, a non-zero tput (common in TERM-less CI) still aborts the script, so the suite never runs. Swallow the failure and fall back to empty strings as previously suggested.

Apply this diff:

-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
-else
-    RED=""
-    GREEN=""
-    NC=""
-fi
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || printf '')
+    GREEN=$(tput setaf 2 2>/dev/null || printf '')
+    NC=$(tput sgr0 2>/dev/null || printf '')
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=''; GREEN=''; NC=''
+    fi
+else
+    RED=''; GREEN=''; NC=''
+fi

234-241: Fail POST test on 5xx/000 responses.

This branch still marks 5xx or curl 000 as PASS, so a broken login slips through. Only 2xx should succeed; everything else must fail.

Apply this diff:

-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
+if [[ "${http_code}" = "404" || "${http_code}" = "403" || "${http_code}" = "000" || "${http_code}" =~ ^5 ]]; then
     printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
     FAILED_TESTS=$(( FAILED_TESTS + 1 ))
     echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
 else
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfb7ea2 and 5b45567.

📒 Files selected for processing (3)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/scripts/test_security.sh
  • tests/scripts/test_performance.sh
🧰 Additional context used
🧬 Code graph analysis (1)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (52-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP (8.4)

Comment on lines +67 to +76
# Check if code matches any of the expected codes
if [[ "${expected_codes}" = *"${http_code}"* ]]; then
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$(( PASSED_TESTS + 1 ))
echo " ✓ ${description}" | tee -a "${REPORT_FILE}"
else
printf "%sFAIL%s (Expected [%s], got %s)\n" "${RED}" "${NC}" "${expected_codes}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$(( FAILED_TESTS + 1 ))
echo " ✗ ${description}" | tee -a "${REPORT_FILE}"
echo " URL: ${url}" | tee -a "${REPORT_FILE}"
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 | 🔴 Critical

Fix status-code matching to avoid false positives.

[[ "${expected_codes}" = *"${http_code}"* ]] treats 000 and other garbage codes as success because they are substrings of 200, so we miss real outages.

Apply this diff:

-    if [[ "${expected_codes}" = *"${http_code}"* ]]; then
+    if [[ " ${expected_codes} " == *" ${http_code} "* ]]; then
📝 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
# Check if code matches any of the expected codes
if [[ "${expected_codes}" = *"${http_code}"* ]]; then
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$(( PASSED_TESTS + 1 ))
echo "${description}" | tee -a "${REPORT_FILE}"
else
printf "%sFAIL%s (Expected [%s], got %s)\n" "${RED}" "${NC}" "${expected_codes}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$(( FAILED_TESTS + 1 ))
echo "${description}" | tee -a "${REPORT_FILE}"
echo " URL: ${url}" | tee -a "${REPORT_FILE}"
# Check if code matches any of the expected codes
if [[ " ${expected_codes} " == *" ${http_code} "* ]]; then
printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
PASSED_TESTS=$(( PASSED_TESTS + 1 ))
echo "${description}" | tee -a "${REPORT_FILE}"
else
printf "%sFAIL%s (Expected [%s], got %s)\n" "${RED}" "${NC}" "${expected_codes}" "${http_code}" | tee -a "${REPORT_FILE}"
FAILED_TESTS=$(( FAILED_TESTS + 1 ))
echo "${description}" | tee -a "${REPORT_FILE}"
echo " URL: ${url}" | tee -a "${REPORT_FILE}"
🤖 Prompt for AI Agents
In tests/scripts/test_compatibility.sh around lines 67-76, the check [[
"${expected_codes}" = *"${http_code}"* ]] falsely matches substrings (e.g. "000"
inside "200"); change the comparison to match whole codes only by treating
expected_codes as a space- or comma-delimited list and checking for a full token
match (for example, pad expected_codes with surrounding separators and test for
" ${http_code} " within it or loop over tokens and compare equality). Ensure
http_code is trimmed and you update the conditional and its branches only (keep
logging/increment behavior unchanged).

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

♻️ Duplicate comments (4)
.env.example (2)

1-1: Remove the shebang line from .env.example.

A .env file is a data file loaded by libraries like phpdotenv, not an executable script. The shebang #!/bin/sh serves no purpose here and could confuse developers about the file's role.

Apply this diff:

-#!/bin/sh
-
 # Possible options are prod or dev. No spaces, no quotes.

9-9: Fix environment variable naming inconsistency.

The new environment variables use a single underscore prefix (OPENEMR_) while existing variables consistently use double underscores (OPENEMR__). This inconsistency was flagged in previous review comments and will cause the front controller code (home.php) to fail when checking for OPENEMR__ENABLE_FRONT_CONTROLLER (double underscore).

Apply this diff to maintain naming consistency:

 # Front controller routing (optional, disabled by default)
 # Set to 1 to enable centralized routing and block .inc.php files
-export OPENEMR_ENABLE_FRONT_CONTROLLER=
+OPENEMR__ENABLE_FRONT_CONTROLLER=
 
 # Front controller logging: debug or empty
-export OPENEMR_FC_LOG_LEVEL=
+OPENEMR__FC_LOG_LEVEL=

Also applies to: 12-12

tests/Tests/FrontController/SecurityTest.php (2)

44-44: Fix: Test scans wrong directory.

dirname(__DIR__, 2) resolves to tests/ instead of the repository root, so the iterator never scans production .inc.php files. This matches the issue flagged in the previous review.

-        $basePath = dirname(__DIR__, 2);
+        $basePath = dirname(__DIR__, 3);

51-51: Fix: Path separator breaks on Windows.

Hardcoded '/' separator will fail on Windows systems. Use DIRECTORY_SEPARATOR for cross-platform compatibility. This matches the issue flagged in the previous review.

-                $relativePath = str_replace($basePath . '/', '', $file->getPathname());
+                $relativePath = str_replace($basePath . DIRECTORY_SEPARATOR, '', $file->getPathname());
🧹 Nitpick comments (3)
tests/Tests/FrontController/SecurityTest.php (3)

21-21: Remove unused import.

GuzzleHttp\Exception\RequestException is imported but never used in this test class.

-use GuzzleHttp\Exception\RequestException;

219-219: Consider using GET instead of HEAD for header verification.

While security headers should be present on HEAD responses, some server configurations or middleware may only send complete headers on GET requests. Using GET would make this test more reliable across different environments.

-        $response = $client->head('/index.php');
+        $response = $client->get('/index.php');

260-260: Consider verifying file write success.

file_put_contents can return false on failure. While the subsequent assertion checks file existence, verifying the write operation itself would catch write permission issues more explicitly.

-        file_put_contents($reportPath, json_encode($report, JSON_PRETTY_PRINT) . "\n");
+        $bytesWritten = file_put_contents($reportPath, json_encode($report, JSON_PRETTY_PRINT) . "\n");
+        $this->assertNotFalse($bytesWritten, 'Failed to write security report');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d01f157 and bdfba05.

📒 Files selected for processing (4)
  • .env.example (1 hunks)
  • interface/login/login.php (1 hunks)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Tests/FrontController/CompatibilityTest.php
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Tests/FrontController/SecurityTest.php (2)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-37)
src/Common/Http/HttpRestRequest.php (2)
  • hasHeader (225-228)
  • getHeaderLine (697-700)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: phpstan (8.4)
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests

Comment thread .env.example
#!/bin/sh
# Possible options are prod or dev. No spaces, no quotes.
OPENEMR__ENVIRONMENT=
export OPENEMR__ENVIRONMENT=
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

Remove export keywords; use standard .env syntax.

The export keyword is shell-specific and incompatible with standard PHP .env parsers like vlucas/phpdotenv. These libraries expect simple KEY=VALUE format without the export prefix. Using export will cause parsing errors or require shell-based loading, which is non-standard for PHP applications.

Apply this diff to use standard .env syntax:

 # Possible options are prod or dev. No spaces, no quotes.
-export OPENEMR__ENVIRONMENT=
+OPENEMR__ENVIRONMENT=
 # Possible options are true to prevent dated reminders and background apps.Else empty or false. No spaces, no quotes.
-export OPENEMR__NO_BACKGROUND_TASKS=
+OPENEMR__NO_BACKGROUND_TASKS=
 
 # Front controller routing (optional, disabled by default)
 # Set to 1 to enable centralized routing and block .inc.php files
-export OPENEMR_ENABLE_FRONT_CONTROLLER=
+OPENEMR__ENABLE_FRONT_CONTROLLER=
 
 # Front controller logging: debug or empty
-export OPENEMR_FC_LOG_LEVEL=
+OPENEMR__FC_LOG_LEVEL=

Also applies to: 5-5, 9-9, 12-12

🤖 Prompt for AI Agents
.in.env.example lines 3,5,9,12: remove the shell-specific "export" prefix from
the listed entries and convert them to standard dotenv KEY=VALUE syntax (e.g.
OPENEMR__ENVIRONMENT=) so PHP .env parsers like vlucas/phpdotenv can read them;
update each affected line by deleting the word "export" and any leading
whitespace so each variable appears as KEY=VALUE on its own line.

Comment thread interface/login/login.php Outdated
Comment on lines +31 to +32
Header("X-Content-Type-Options: nosniff");
Header("X-XSS-Protection: 1; mode=block");
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

X-XSS-Protection header is deprecated; consider removing it.

The X-XSS-Protection header was deprecated and removed from modern browsers (Chrome in 2019, Edge follows, Firefox never implemented it). Modern browsers rely on Content Security Policy (CSP) instead. While X-Content-Type-Options: nosniff is appropriate and recommended, the XSS Protection header serves no purpose and could create a false sense of security.

Consider removing line 32 or documenting that it's only for legacy browser support.

Additionally, since the AI summary indicates that multiple entry points (index.php, login.php) are adding similar security headers, consider centralizing these headers in the front controller or a shared security middleware to avoid duplication and ensure consistency.

Apply this diff to remove the deprecated header:

 // prevent UI redressing and set security headers
 Header("X-Frame-Options: DENY");
 Header("Content-Security-Policy: frame-ancestors 'none'");
 Header("X-Content-Type-Options: nosniff");
-Header("X-XSS-Protection: 1; mode=block");
🤖 Prompt for AI Agents
interface/login/login.php around lines 31-32: remove the deprecated
Header("X-XSS-Protection: 1; mode=block") line (keep the X-Content-Type-Options
header), and instead centralize all common security headers into the front
controller or a shared security middleware (e.g., a single include loaded by
index.php/login.php) so headers aren’t duplicated across entry points; delete
the deprecated header line here and add the remaining necessary headers in the
centralized file, then remove duplicates from other entry files.

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

♻️ Duplicate comments (4)
tests/Tests/FrontController/CompatibilityTest.php (3)

20-22: Remove unused import.

Cleanup unused RequestException import.

 use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
 use PHPUnit\Framework\TestCase;

208-214: Use version‑agnostic assertion for status set membership.

assertTrue + in_array avoids PHPUnit version deprecations.

-        // Setup should be accessible (200) or redirect (302)
-        $this->assertContains(
-            $httpCode,
-            [200, 302],
-            'Setup workflow should be accessible'
-        );
+        // Setup should be accessible (200) or redirect (302)
+        $this->assertTrue(
+            in_array($httpCode, [200, 302], true),
+            'Setup workflow should be accessible'
+        );

269-275: Use version‑agnostic assertion for status set membership.

Same rationale as above.

-        // POST should return 200 or 302 (redirect)
-        $this->assertContains(
-            $httpCode,
-            [200, 302],
-            'POST requests should be processed'
-        );
+        // POST should return 200 or 302 (redirect)
+        $this->assertTrue(
+            in_array($httpCode, [200, 302], true),
+            'POST requests should be processed'
+        );
tests/Tests/FrontController/SecurityTest.php (1)

45-55: Fix repo-root path and make relative path computation cross‑platform.

Iterator starts at tests/ instead of repo root and Windows paths break due to hardcoded '/'. Apply below to walk from repo root, skip dots, and normalize separators. This also addresses prior feedback.

-        // Walk up to repository root to scan production .inc.php files
-        $basePath = dirname(__DIR__, 2);
-        $iterator = new \RecursiveIteratorIterator(
-            new \RecursiveDirectoryIterator($basePath)
-        );
+        // Walk up to repository root to scan production .inc.php files
+        $basePath = dirname(__DIR__, 3);
+        $iterator = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($basePath, \FilesystemIterator::SKIP_DOTS)
+        );
@@
-        foreach ($iterator as $file) {
-            if ($file->isFile() && preg_match('/\.inc\.php$/i', $file->getFilename())) {
-                $relativePath = str_replace($basePath . '/', '', $file->getPathname());
-                self::$vulnerable_inc_files[] = $relativePath;
-            }
-        }
+        foreach ($iterator as $file) {
+            if ($file->isFile() && preg_match('/\.inc\.php$/i', $file->getFilename())) {
+                // Cross‑platform relative path (normalize separators)
+                $baseNorm = rtrim(str_replace('\\', '/', realpath($basePath)), '/');
+                $pathNorm = str_replace('\\', '/', $file->getPathname());
+                $relativePath = strpos($pathNorm, $baseNorm . '/') === 0
+                    ? substr($pathNorm, strlen($baseNorm) + 1)
+                    : $pathNorm;
+                self::$vulnerable_inc_files[] = $relativePath;
+            }
+        }
🧹 Nitpick comments (4)
tests/Tests/FrontController/SecurityTest.php (4)

60-62: Skip test if no .inc.php files found (avoid false green).

         // Test a sample of vulnerable .inc.php files
         $sampleFiles = array_slice(self::$vulnerable_inc_files, 0, 10);
+        if (empty($sampleFiles)) {
+            $this->markTestSkipped('No .inc.php files found to test');
+        }

21-22: Remove unused import.

 use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
 use PHPUnit\Framework\TestCase;

212-243: Modernize header assertions (X‑XSS‑Protection is deprecated).

Prefer asserting CSP (and optionally Referrer‑Policy/Permissions‑Policy). Accept X‑XSS‑Protection or CSP to avoid brittle failures across environments.

-        $this->assertTrue(
-            $response->hasHeader('X-XSS-Protection'),
-            'Security header X-XSS-Protection should be present'
-        );
+        // X-XSS-Protection is deprecated in modern browsers; accept CSP as an alternative
+        $this->assertTrue(
+            $response->hasHeader('X-XSS-Protection') || $response->hasHeader('Content-Security-Policy'),
+            'Expect X-XSS-Protection or Content-Security-Policy to be present'
+        );

60-73: HEAD may behave inconsistently; consider HEAD→GET fallback.

Some endpoints return 405/404 for HEAD though GET would be 200/403. Add a helper to try HEAD then fallback to GET to reduce test flakiness.

Also applies to: 80-89

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8df87 and 632b6cc.

📒 Files selected for processing (2)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-41)
tests/Tests/FrontController/SecurityTest.php (1)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
  • GitHub Check: PHP 8.3 - Isolated Tests

Comment thread tests/Tests/FrontController/SecurityTest.php
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

🧹 Nitpick comments (3)
src/OpenCoreEMR/FrontController/Router.php (3)

37-49: Consider validating and sanitizing the route parameter.

The route from $_GET['_ROUTE'] is used without validation. While resolveTargetFile() provides path traversal protection via realpath(), validating input early can prevent issues and improve security posture. Consider rejecting routes with null bytes, excessive path segments, or suspicious patterns.

Example validation to add after line 45:

        // Remove query string
        $route = strtok($route, '?');

        // Basic validation
        if (str_contains($route, "\0") || preg_match('/[<>"|*?]/', $route)) {
            $route = '';
        }

        $this->route = $route;

51-61: Validate the site parameter to prevent injection.

The site parameter from $_GET is stored without validation. Consider validating against a whitelist or checking for malicious patterns (null bytes, path traversal sequences, etc.) to prevent potential injection attacks.

Example validation:

     public function determineSiteId(): string
     {
         // explicit site parameter or session-based detection.
-        $this->siteId = $_GET['site'] ?? 'default';
+        $site = $_GET['site'] ?? 'default';
+        
+        // Validate site parameter (alphanumeric, hyphens, underscores only)
+        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $site)) {
+            $site = 'default';
+        }
+        
+        $this->siteId = $site;

         $_GET['site'] = $this->siteId;
         return $this->siteId;
     }

56-56: Update or remove incomplete comment.

The comment mentions "session-based detection" but the implementation only uses $_GET['site']. Either implement session-based fallback or update the comment to reflect the actual behavior.

-        // explicit site parameter or session-based detection.
+        // Use explicit site parameter from query string, defaulting to 'default'
         $this->siteId = $_GET['site'] ?? 'default';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 632b6cc and 8673b9c.

📒 Files selected for processing (2)
  • src/Events/FrontController/FrontControllerEvent.php (1 hunks)
  • src/OpenCoreEMR/FrontController/Router.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Events/FrontController/FrontControllerEvent.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/OpenCoreEMR/FrontController/Router.php (2)
src/Events/FrontController/FrontControllerEvent.php (3)
  • __construct (46-60)
  • getRoute (67-70)
  • getSiteId (77-80)
src/OpenCoreEMR/FrontController/RouteConfig.php (5)
  • __construct (43-46)
  • RouteConfig (20-236)
  • isDeprecated (148-151)
  • isForbidden (126-129)
  • requiresAdmin (137-140)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests
🔇 Additional comments (4)
src/OpenCoreEMR/FrontController/Router.php (4)

27-35: LGTM! Constructor validation is solid.

The realpath() validation and exception handling address the previous review concerns. The error message is clear, and the fallback to a default RouteConfig is appropriate.


80-94: LGTM! Configuration delegation is clean.

Both isForbiddenPath() and requiresAdmin() properly delegate to the configuration interface, maintaining good separation of concerns.


96-119: LGTM! Path traversal protection is robust.

The resolveTargetFile() method properly addresses previous security concerns:

  • Uses str_starts_with() with DIRECTORY_SEPARATOR for path traversal prevention
  • Validates file existence before returning
  • Restricts routing to .php files only

This provides strong security guarantees for file resolution.


121-135: LGTM! Getters are correctly implemented.

The getter methods are straightforward and correctly return the internal state. The uninitialized property concern was addressed in the earlier comment on the property declarations.

Comment on lines +22 to +24
private readonly string $baseDir;
private string $route;
private string $siteId;
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

Initialize $route and $siteId properties to prevent uninitialized access.

Properties $route and $siteId are declared but not initialized in the constructor. If getRoute() or getSiteId() are called before extractRoute() or determineSiteId(), this will throw an error (uninitialized typed property in PHP 8.1+).

Apply this diff to initialize the properties:

     private readonly string $baseDir;
-    private string $route;
-    private string $siteId;
+    private string $route = '';
+    private string $siteId = 'default';
     private readonly RouteConfigInterface $config;
📝 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
private readonly string $baseDir;
private string $route;
private string $siteId;
private readonly string $baseDir;
private string $route = '';
private string $siteId = 'default';
private readonly RouteConfigInterface $config;
🤖 Prompt for AI Agents
In src/OpenCoreEMR/FrontController/Router.php around lines 22 to 24, the typed
properties $route and $siteId are declared but not initialized which can cause
an "uninitialized typed property" error if getters are called before
extractRoute()/determineSiteId(); initialize them to safe defaults (e.g., empty
strings) either by assigning default values at declaration or by assigning them
in the constructor so getRoute() and getSiteId() always return initialized
values.

Comment on lines +63 to +78
/**
* Handle trailing slash redirect
*
* We always want there to be a trailing slash for consistency
*/
public function handleTrailingSlash(): void
{
// Skip trailing slash redirect for .php files
if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
// Ensure proper relative redirect with query string preservation
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
header('Location: ' . $redirectUrl, true, 301);
exit;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Query string preservation is correct, but validate the redirect URL.

The query string preservation logic addresses the previous review concern. However, ensure $this->route doesn't contain absolute URLs or protocol-relative URLs before constructing the redirect. If an attacker can control the route to be //evil.com, the redirect could send users to an external site.

Add validation before the redirect:

     public function handleTrailingSlash(): void
     {
         // Skip trailing slash redirect for .php files
         if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
+            // Prevent open redirect: ensure route is relative
+            if (str_starts_with($this->route, '//') || str_contains($this->route, '://')) {
+                return;
+            }
+            
             // Ensure proper relative redirect with query string preservation
             $queryString = $_SERVER['QUERY_STRING'] ?? '';
             $redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
             header('Location: ' . $redirectUrl, true, 301);
             exit;
         }
     }

Run the following script to check if there are existing guards against absolute URLs in the route extraction:


🏁 Script executed:

#!/bin/bash
# Search for route validation or sanitization in related files
rg -nP --type=php -C3 '(https?://|protocol.*relative|absolute.*url|open.*redirect)' src/OpenCoreEMR/FrontController/

Length of output: 2502


Add open-redirect protection in handleTrailingSlash

Ensure $this->route is a relative path before redirecting:

     public function handleTrailingSlash(): void
     {
         // Skip trailing slash redirect for .php files
         if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
+            // Prevent open redirect: route must not start with "//" or contain a scheme
+            if (str_starts_with($this->route, '//') || str_contains($this->route, '://')) {
+                return;
+            }
             // Ensure proper relative redirect with query string preservation
             $queryString = $_SERVER['QUERY_STRING'] ?? '';
             $redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
             header('Location: ' . $redirectUrl, true, 301);
             exit;
         }
     }
📝 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
/**
* Handle trailing slash redirect
*
* We always want there to be a trailing slash for consistency
*/
public function handleTrailingSlash(): void
{
// Skip trailing slash redirect for .php files
if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
// Ensure proper relative redirect with query string preservation
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
header('Location: ' . $redirectUrl, true, 301);
exit;
}
}
/**
* Handle trailing slash redirect
*
* We always want there to be a trailing slash for consistency
*/
public function handleTrailingSlash(): void
{
// Skip trailing slash redirect for .php files
if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
// Prevent open redirect: route must not start with "//" or contain a scheme
if (str_starts_with($this->route, '//') || str_contains($this->route, '://')) {
return;
}
// Ensure proper relative redirect with query string preservation
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
header('Location: ' . $redirectUrl, true, 301);
exit;
}
}
🤖 Prompt for AI Agents
In src/OpenCoreEMR/FrontController/Router.php around lines 63 to 78, the
trailing-slash redirect currently uses $this->route directly which can enable an
open-redirect; before issuing the header Location, verify the route is a safe
internal path by ensuring it does not contain a URL scheme
(parse_url($this->route, PHP_URL_SCHEME) is null), is not protocol-relative
(does not start with '//'), and is an internal path (e.g. starts with '/'); only
perform the redirect when those checks pass, otherwise skip the redirect (or
return) to prevent open-redirects.

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

♻️ Duplicate comments (1)
tests/Tests/FrontController/SecurityTest.php (1)

47-47: Scan the real application tree.
dirname(__DIR__, 2) still resolves to the tests/ directory, so the iterator never touches production .inc.php files and the test misses regressions. Walk up one more level (e.g., dirname(__DIR__, 3) or anchoring on project root) before scanning.

🧹 Nitpick comments (1)
tests/Tests/FrontController/SecurityTest.php (1)

54-55: Normalize separators when deriving relative paths.
str_replace($basePath . '/', …) only works on POSIX paths; Windows backslashes leave prefixes intact and the test requests /tests\foo.inc.php. Use DIRECTORY_SEPARATOR (or substr/ltrim) to produce portable URLs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8673b9c and d61da95.

📒 Files selected for processing (2)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/Tests/FrontController/SecurityTest.php (1)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-40)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: PHP 8.5 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.3 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 5.7
  • GitHub Check: PHP 8.3 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 11.4
  • GitHub Check: PHP 8.4 - apache - mysql 8.0
  • GitHub Check: PHP 8.6 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 9.3
  • GitHub Check: PHP 8.2 - nginx - mariadb 11.8
  • GitHub Check: PHP 8.2 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mysql 8.4
  • GitHub Check: PHP 8.4 - apache - mariadb 10.11
  • GitHub Check: PHP 8.4 - apache - mariadb 11.8
  • GitHub Check: PHP 8.4 - apache - mariadb 10.6
  • GitHub Check: PHP 8.3 - Isolated Tests
  • GitHub Check: PHP 8.2 - Isolated Tests
  • GitHub Check: PHP 8.5 - Isolated Tests
  • GitHub Check: PHP 8.4 - Isolated Tests

Comment on lines +97 to +116
$attackVectors = [
'../../../etc/passwd',
'....//....//....//etc/passwd',
'..%2F..%2F..%2Fetc%2Fpasswd',
'..%5c..%5c..%5cetc%5cpasswd',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
];

foreach ($attackVectors as $vector) {
$response = self::$client->head('/home.php?_ROUTE=' . urlencode($vector), [
'allow_redirects' => false
]);
$httpCode = $response->getStatusCode();

$this->assertEquals(
404,
$httpCode,
"Path traversal attack '{$vector}' should return 404, got {$httpCode}"
);
}
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

Stop double-encoding traversal payloads.
Already escaped vectors (those containing %) get urlencode()d again, turning % into %25 and weakening coverage. Keep raw vectors (encode them once) and pass pre-encoded ones through untouched to exercise the real attack strings.

🤖 Prompt for AI Agents
In tests/Tests/FrontController/SecurityTest.php around lines 97 to 116, the test
double-encodes already percent-encoded attack vectors by always calling
urlencode(), which turns '%' into '%25' and defeats coverage; change the request
assembly so that vectors containing '%' (pre-encoded) are used as-is while raw
vectors are encoded once (e.g., if strpos($vector, '%') !== false then use
$vector else urlencode($vector)), then build the URL with the chosen value so
pre-encoded payloads are not altered and raw payloads remain properly encoded.

@coding-sunshine coding-sunshine force-pushed the 8964-global-front-controller branch from 4326c3b to 94b8a1a Compare October 29, 2025 05:40
Implements centralized routing architecture with multi-layer security:

  - Add home.php as main front controller for centralized routing
  - Add .htaccess with Apache rewrite rules for request routing
  - Add security-check.php as auto-prepended .inc.php blocker
  - Block direct access to .inc.php files (403 Forbidden)
  - Deny access to sensitive paths (/portal/patient/fwk/libs/, /sites/*/documents/)
  - Preserve existing inner front controllers (/apis/, /portal/, /oauth2/, /interface/*)
  - Add OPENEMR_ENABLE_FRONT_CONTROLLER feature flag (disabled by default)
  - Add extension hooks for early/late custom logic injection
  - Add multisite support with automatic site detection
  - Add path traversal prevention with realpath() validation
  - Add admin path detection for telemetry integration
  - Add comprehensive test suite (PHPUnit + shell scripts)
  - Add README-FRONT-CONTROLLER.md with deployment guide

  Security improvements:
  - Three-layer .inc.php blocking (.htaccess → security-check.php → home.php)
  - Request validation before routing
  - Error logging for security events
  - Debug mode via OPENEMR_FC_LOG_LEVEL

  Backward compatibility:
  - Feature disabled by default (opt-in via environment variable)
  - No breaking changes to existing routing
  - All existing front controllers preserved
  - Static assets passed through unchanged

  Files added:
  - .htaccess (Apache routing rules)
  - home.php (main front controller)
  - security-check.php (PHP prepend security layer)
  - README-FRONT-CONTROLLER.md (documentation)
  - custom/.gitkeep (extension directory)
  - tests/FrontController/SecurityTest.php
  - tests/FrontController/CompatibilityTest.php
  - tests/scripts/test_security.sh
  - tests/scripts/test_compatibility.sh
  - tests/scripts/test_performance.sh

  Files modified:
  - .env.example (feature flag documentation)
…tation

ARCHITECTURAL IMPROVEMENTS:
- Extracted routing logic from home.php into dedicated Router class (src/OpenCoreEMR/FrontController/Router.php)
- Created RouteConfig class for centralized route configuration (src/OpenCoreEMR/FrontController/RouteConfig.php)
- Delegated security validation to SecurityValidator class
- Implemented event system hooks for module extensibility
- Removed global variables, improved testability and maintainability

ROUTING SYSTEM:
- Route patterns now centralized in RouteConfig with clear categorization
- Admin paths, forbidden paths, bypass patterns all configurable
- Event system allows modules to extend routing rules dynamically
- Removed hardcoded path matching from home.php

CI INTEGRATION:
- Integrated FrontController tests with PHPUnit CI pipeline (phpunit.xml)
- Created dedicated 'frontcontroller' test suite for targeted testing
- Added tests to main 'openemr' suite for comprehensive coverage
- Enabled CLI test execution via OPENEMR_TEST_URL environment variable

TEST QUALITY IMPROVEMENTS:
- Replaced curl with Guzzle HTTP client in CompatibilityTest.php
- Fixed HTTP assertions to be specific per endpoint (200, 302, 401, 400)
- Moved tests to PSR-4 compliant location (tests/Tests/FrontController/)
- Added proper client setup with base_uri and error handling configuration

SHELL SCRIPT QUALITY (POSIX compliance):
- Replaced ANSI escape sequences with tput commands for portability
- Changed echo -n/-e to printf for POSIX compliance
- Replaced [ ] with [[ ]] for consistency and safety
- Changed == to = for string comparisons (POSIX compliant)
- Consolidated echo statements into compound commands
- Used herestrings instead of echo | pipe for efficiency
- Removed auto-install logic for security best practices

DOCUMENTATION:
- Updated architecture diagram showing Router/RouteConfig delegation
- Added extensible configuration examples with event system
- Updated file counts (6→8 core files, correct test paths)
- Added troubleshooting section (.htaccess flags, performance, CLI)
- Documented CI integration commands and test execution
- Added FAQ about extending routing rules via Event System

SECURITY:
- Maintained all security validations (path traversal, .inc.php blocking)
- Preserved feature flag controls and multisite selection
- No changes to security-check.php auto-prepend mechanism
- All security patterns now in RouteConfig for easier auditing

BACKWARD COMPATIBILITY:
- Zero functional changes to routing behavior
- All existing front controllers preserved (/apis/, /portal/, /oauth2/)
- Static assets still bypass routing
- Extension hooks remain optional

Addresses Client Review Comments #1-21:
… the disabled condition.

The `.htaccess` routing is designed to work in both states:

  ```apache
  # Physical files pass through directly (never hit home.php)
  RewriteCond %{REQUEST_FILENAME} -f
  RewriteRule ^ - [L]

  # Only non-existent files route to home.php
  RewriteCond %{REQUEST_URI} \.php$
  RewriteRule ^(.*)$ home.php?_ROUTE=$1 [QSA,L]

  When Front Controller is DISABLED:

  -  Existing PHP files (interface/login/login.php, etc.): Served directly by Apache via the -f check
  -  Virtual routes: Pass through home.php with transparent context preservation
  -  .inc.php files: Still blocked for security

  When Front Controller is ENABLED:

  -  All routes: Go through front controller with full security validation
  -  Security checks: Router validates paths, blocks forbidden files

  The Transparent Pass-Through

  For the edge case where disabled mode receives routed requests, home.php now acts as a transparent proxy:

  // Preserves correct execution context
  $_SERVER['SCRIPT_FILENAME'] = $targetFile;
  $_SERVER['SCRIPT_NAME'] = '/' . $route;
  $_SERVER['PHP_SELF'] = '/' . $route;
  chdir(dirname($targetFile));

  // Security maintained even when disabled
  - Directory traversal prevention
  - .inc.php blocking
  - File existence validation

  require $targetFile;  // __FILE__ and __DIR__ work correctly in target

  Tested and verified: __FILE__, __DIR__, relative paths, and $_SERVER variables all work correctly for the target file.

  The .htaccess + home.php combination now handles both enabled and disabled states correctly.
Fixed PHPStan errors in RouteConfig.php:
  - Renamed addForbiddenPattern() → addForbiddenPath() to match interface
  - Renamed addAdminPattern() → addAdminPath() to match interface
  - Replaced non-existent array_any() with standard foreach loop

  Fixed ShellCheck warnings in test scripts:
  - Added tput fallback for non-interactive environments in test_compatibility.sh
  - Added tput fallback for non-interactive environments in test_performance.sh
coding-sunshine and others added 15 commits October 30, 2025 17:25
Add spaces in arithmetic expressions to comply with ShellCheck SC2250.
Changed all arithmetic from $((VAR + 1)) to $(( VAR + 1 )) format.

Changes:
- test_compatibility.sh: Fixed arithmetic spacing on lines 56, 66, 70, 223, 232, 236
- test_security.sh: Fixed arithmetic spacing on lines 54, 62, 66, 207, 216, 220, 233, 240

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- test_performance.sh: Remove orphaned $YELLOW reference, add braces to all variables
- test_security.sh: Add braces to all variable references including $headers and $response
- test_compatibility.sh: Already compliant with SC2250

ShellCheck SC2250 requires braces on ALL variable references throughout shell scripts,
not just in specific contexts. All $VAR references changed to ${VAR}.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Address SC2312: "Consider invoking this command separately to avoid masking
its return value" by refactoring command substitutions in test conditions.

Changes:
- test_performance.sh: Separate bc command substitutions from test conditions
  in evaluate_overhead() and overhead calculation to avoid masking exit codes
- test_security.sh: Replace piped echo commands with here-strings for grep
  to prevent masking return values in security header checks

All changes preserve existing functionality while ensuring proper error
handling and exit code visibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Address multiple ShellCheck warnings to improve script reliability:

SC2311 - set -e disabled in command substitutions:
- Add 'shopt -s inherit_errexit' to all test scripts to ensure error
  handling works correctly in command substitutions
- Prevents silent failures when functions are called via $()

SC2034 - unused variables:
- Prefix unused login_time variable with underscore (_login_time)
- Indicates intentionally unused value from command output

SC2009 - avoid grepping ps output:
- Replace 'ps aux | grep php' with 'pgrep -c php'
- More reliable and portable process counting

All changes maintain backward compatibility and improve error handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Address ShellCheck SC2312 warnings by ensuring command substitutions
are executed separately before being used in pipes, preventing masked
return values and improving error detection.

Changes:
- Extract date command outside of pipe blocks in all test scripts
- Move uname check outside command substitution in CPU detection
- Refactor top command execution to separate steps
- Build throughput output string before piping to tee
- Replace inline grep pipes with here-strings for top output parsing

All changes maintain the same functionality while ensuring proper
error handling and exit code visibility as required by ShellCheck.
Split grep|awk pipelines in CPU usage detection to avoid masking
return values. Extract grep output first, then conditionally apply
awk to prevent SC2312 warnings.

Changes:
- macOS: separate 'grep CPU usage' and 'awk' into distinct steps
- Linux: separate 'grep Cpu(s)' and 'awk' into distinct steps
- Add conditional checks for grep output before awk processing
- Maintain same error handling with fallback to N/A
Removed redundant security headers from home.php, index.php, and login.php, centralizing header management. Refactored event dispatching in home.php to use a helper function for late event dispatch. Updated license comments to reference the full GPLv3 license URL. Improved type hints and return types in FrontControllerEvent. Standardized path concatenation using DIRECTORY_SEPARATOR and simplified baseDir validation in Router.
- Remove testSecurityHeadersPresent() test as headers will be in separate PR
- Update license format to full URL in test files for consistency
@coding-sunshine coding-sunshine force-pushed the 8964-global-front-controller branch from 3a8f9e4 to 64cc024 Compare October 30, 2025 06:25
- Remove security header test from test_security.sh script
- Update license format to full URL in all remaining files:
  - security-check.php
  - test_security.sh
  - test_performance.sh
  - test_compatibility.sh
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

♻️ Duplicate comments (12)
security-check.php (1)

17-18: Block raw .inc files too.

The regex only matches .inc.php and .config.php, but raw .inc files (e.g., foo.inc) are not blocked. This is inconsistent with SecurityValidator::isIncludeFile() at src/OpenCoreEMR/FrontController/SecurityValidator.php:35 which blocks both .inc and .inc.php.

Apply this diff:

-if (preg_match('/\.(?:inc|config)\.php$/i', $requestedFile) || preg_match('#/(?:includes?|inc)/#i', $requestedFile)) {
+if (
+    preg_match('/\.inc(?:\.php)?$/i', $requestedFile) ||
+    preg_match('/\.config\.php$/i', $requestedFile) ||
+    preg_match('#/(?:includes?|inc)/#i', $requestedFile)
+) {
src/OpenCoreEMR/FrontController/Router.php (1)

68-78: Prevent open redirect vulnerability.

The redirect uses $this->route without validating that it's a relative internal path. An attacker could set _ROUTE=//evil.com to redirect users to an external site.

Apply this diff to add validation:

     public function handleTrailingSlash(): void
     {
         // Skip trailing slash redirect for .php files
         if ($this->route !== '' && !str_ends_with($this->route, '/') && pathinfo($this->route, PATHINFO_EXTENSION) !== 'php') {
+            // Prevent open redirect: ensure route is relative
+            if (str_starts_with($this->route, '//') || str_contains($this->route, '://')) {
+                return;
+            }
             // Ensure proper relative redirect with query string preservation
             $queryString = $_SERVER['QUERY_STRING'] ?? '';
             $redirectUrl = $this->route . '/' . ($queryString ? '?' . $queryString : '');
             header('Location: ' . $redirectUrl, true, 301);
             exit;
         }
     }
tests/scripts/test_compatibility.sh (3)

25-34: Handle tput failures gracefully.

With set -e, any tput failure (common when TERM lacks color support) will terminate the script before tests run. Redirect stderr and provide fallbacks.

Apply this diff:

-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || printf '')
+    GREEN=$(tput setaf 2 2>/dev/null || printf '')
+    NC=$(tput sgr0 2>/dev/null || printf '')
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
 else
-    RED=""
-    GREEN=""
-    NC=""
+    RED=""; GREEN=""; NC=""
 fi

67-68: Fix status code matching to prevent false positives.

The substring match [[ "${expected_codes}" = *"${http_code}"* ]] treats "000" as matching "200", causing false positives when curl fails.

Apply this diff to match whole codes only:

-    if [[ "${expected_codes}" = *"${http_code}"* ]]; then
+    if [[ " ${expected_codes} " == *" ${http_code} "* ]]; then

234-242: Tighten POST test to fail on server errors.

The current logic treats any response except 403/404 as success, so 5xx errors and curl failures (000) incorrectly pass.

Apply this diff to only pass on 2xx:

-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
+if [[ "${http_code}" =~ ^2 ]]; then
+    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
+    PASSED_TESTS=$(( PASSED_TESTS + 1 ))
+    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
+else
     printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
     FAILED_TESTS=$(( FAILED_TESTS + 1 ))
     echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
-else
-    printf "%sPASS%s (HTTP %s)\n" "${GREEN}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
-    PASSED_TESTS=$(( PASSED_TESTS + 1 ))
-    echo "  ✓ POST requests are processed correctly" | tee -a "${REPORT_FILE}"
 fi
tests/Tests/FrontController/SecurityTest.php (4)

18-22: Remove unused import.

GuzzleHttp\Exception\RequestException is imported but never referenced in this test class.

Apply this diff:

 namespace OpenCoreEMR\Tests\FrontController;
 
 use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
 use PHPUnit\Framework\TestCase;

53-54: Use DIRECTORY_SEPARATOR for cross-platform compatibility.

Hardcoding / in path replacement fails on Windows systems where the directory separator is \.

Apply this diff:

-                $relativePath = str_replace($basePath . '/', '', $file->getPathname());
+                $relativePath = str_replace($basePath . DIRECTORY_SEPARATOR, '', $file->getPathname());

44-58: Fix base path to scan repository root, not tests/ folder.

dirname(__DIR__, 2) from tests/Tests/FrontController/SecurityTest.php walks up only to tests/, so the iterator never scans production .inc.php files. The self::$vulnerable_inc_files array stays empty, and testIncPhpFilesBlocked cannot detect regressions.

Apply this diff:

-        $basePath = dirname(__DIR__, 2);
+        $basePath = dirname(__DIR__, 3);

97-119: Separate raw and pre-encoded traversal vectors to avoid double-encoding.

Calling urlencode() on already percent-encoded vectors (containing %2F, %5c, %2e) transforms % into %25, which weakens test coverage by not exercising the actual attack strings.

Apply this diff to split the vectors:

+        // Raw vectors that need encoding
+        $rawVectors = [
+            '../../../etc/passwd',
+            '....//....//....//etc/passwd',
+        ];
+        
+        // Pre-encoded vectors to test as-is
+        $encodedVectors = [
+            '..%2F..%2F..%2Fetc%2Fpasswd',
+            '..%5c..%5c..%5cetc%5cpasswd',
+            '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
+        ];
+
-        $attackVectors = [
-            '../../../etc/passwd',
-            '....//....//....//etc/passwd',
-            '..%2F..%2F..%2Fetc%2Fpasswd',
-            '..%5c..%5c..%5cetc%5cpasswd',
-            '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
-        ];
-
-        foreach ($attackVectors as $vector) {
-            $response = self::$client->head('/home.php?_ROUTE=' . urlencode($vector), [
+        foreach ($rawVectors as $vector) {
+            $response = self::$client->head('/home.php?_ROUTE=' . urlencode($vector), [
+                'allow_redirects' => false
+            ]);
+            $httpCode = $response->getStatusCode();
+            $this->assertEquals(
+                404,
+                $httpCode,
+                "Path traversal attack '{$vector}' should return 404, got {$httpCode}"
+            );
+        }
+
+        foreach ($encodedVectors as $vector) {
+            $response = self::$client->head('/home.php?_ROUTE=' . $vector, [
                 'allow_redirects' => false
             ]);
             $httpCode = $response->getStatusCode();
-
             $this->assertEquals(
                 404,
                 $httpCode,
tests/Tests/FrontController/CompatibilityTest.php (3)

18-22: Remove unused import.

GuzzleHttp\Exception\RequestException is imported but never used in this test class.

Apply this diff:

 namespace OpenCoreEMR\Tests\FrontController;
 
 use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
 use PHPUnit\Framework\TestCase;

202-215: Use PHPUnit 9+ compatible assertion.

assertContains is deprecated in PHPUnit 9+ for checking array membership. Use assertContainsEquals or restructure the assertion.

Apply this diff:

-        $this->assertContains(
-            $httpCode,
-            [200, 302],
-            'Setup workflow should be accessible'
-        );
+        $this->assertThat(
+            $httpCode,
+            $this->logicalOr(
+                $this->equalTo(200),
+                $this->equalTo(302)
+            ),
+            'Setup workflow should be accessible'
+        );

Or alternatively:

-        $this->assertContains(
-            $httpCode,
-            [200, 302],
-            'Setup workflow should be accessible'
-        );
+        $this->assertTrue(
+            in_array($httpCode, [200, 302]),
+            "Setup workflow should be accessible (expected 200 or 302, got {$httpCode})"
+        );

259-276: Use PHPUnit 9+ compatible assertion.

assertContains is deprecated in PHPUnit 9+ for checking array membership.

Apply this diff:

-        $this->assertContains(
-            $httpCode,
-            [200, 302],
-            'POST requests should be processed'
-        );
+        $this->assertTrue(
+            in_array($httpCode, [200, 302]),
+            "POST requests should be processed (expected 200 or 302, got {$httpCode})"
+        );
🧹 Nitpick comments (2)
.github/workflows/test.yml (1)

829-833: Consider adding coverage collection for FrontController tests.

Unlike other test suites (unit, api, fixtures, etc.), the FrontController test step doesn't include coverage collection and upload steps. If coverage is intended, add steps to copy/convert coverage files and upload to Codecov, following the pattern used in lines 462-502 for API tests.

tests/scripts/test_compatibility.sh (1)

227-243: Consider refactoring POST test to use run_test.

The POST test manually manages counters and logic instead of using the run_test helper. Consider refactoring to use the helper for consistency, or document why manual handling is needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a8f9e4 and 64cc024.

📒 Files selected for processing (24)
  • .env.example (1 hunks)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • .htaccess (1 hunks)
  • README-FRONT-CONTROLLER.md (1 hunks)
  • composer.json (1 hunks)
  • home.php (1 hunks)
  • index.php (1 hunks)
  • interface/login/login.php (1 hunks)
  • phpunit.xml (2 hunks)
  • reports/.gitkeep (1 hunks)
  • security-check.php (1 hunks)
  • src/Events/FrontController/FrontControllerEvent.php (1 hunks)
  • src/OpenCoreEMR/FrontController/RouteConfig.php (1 hunks)
  • src/OpenCoreEMR/FrontController/RouteConfigInterface.php (1 hunks)
  • src/OpenCoreEMR/FrontController/Router.php (1 hunks)
  • src/OpenCoreEMR/FrontController/SecurityValidator.php (1 hunks)
  • tests/Tests/FrontController/CompatibilityTest.php (1 hunks)
  • tests/Tests/FrontController/SecurityTest.php (1 hunks)
  • tests/reports/compatibility-test-report.json (1 hunks)
  • tests/reports/security-test-report.json (1 hunks)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (13)
  • tests/reports/security-test-report.json
  • src/Events/FrontController/FrontControllerEvent.php
  • tests/reports/compatibility-test-report.json
  • interface/login/login.php
  • tests/scripts/test_performance.sh
  • reports/.gitkeep
  • src/OpenCoreEMR/FrontController/RouteConfigInterface.php
  • tests/scripts/test_security.sh
  • composer.json
  • .env.example
  • .htaccess
  • src/OpenCoreEMR/FrontController/RouteConfig.php
  • index.php
🧰 Additional context used
🧬 Code graph analysis (6)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (52-77)
home.php (3)
src/OpenCoreEMR/FrontController/Router.php (8)
  • Router (20-136)
  • determineSiteId (54-61)
  • extractRoute (40-49)
  • handleTrailingSlash (68-78)
  • isForbiddenPath (83-86)
  • requiresAdmin (91-94)
  • resolveTargetFile (99-119)
  • getSiteId (132-135)
src/OpenCoreEMR/FrontController/SecurityValidator.php (4)
  • SecurityValidator (20-53)
  • logSecurityEvent (41-44)
  • isIncludeFile (33-36)
  • getDebugMessage (49-52)
src/Events/FrontController/FrontControllerEvent.php (2)
  • FrontControllerEvent (23-111)
  • getSiteId (61-64)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-42)
src/OpenCoreEMR/FrontController/Router.php (3)
src/Events/FrontController/FrontControllerEvent.php (3)
  • __construct (39-44)
  • getRoute (51-54)
  • getSiteId (61-64)
src/OpenCoreEMR/FrontController/RouteConfig.php (5)
  • __construct (43-46)
  • RouteConfig (20-236)
  • isDeprecated (148-151)
  • isForbidden (126-129)
  • requiresAdmin (137-140)
src/OpenCoreEMR/FrontController/RouteConfigInterface.php (3)
  • isDeprecated (65-65)
  • isForbidden (49-49)
  • requiresAdmin (57-57)
security-check.php (1)
library/classes/thumbnail/ThumbnailGenerator.php (1)
  • error_log (212-216)
tests/Tests/FrontController/SecurityTest.php (1)
tests/Tests/FrontController/CompatibilityTest.php (1)
  • setUpBeforeClass (29-40)
🪛 LanguageTool
README-FRONT-CONTROLLER.md

[uncategorized] ~139-~139: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...lemetry - Custom authentication layer - Rate limiting - Request preprocessing **Example - Re...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
README-FRONT-CONTROLLER.md

10-10: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (28)
security-check.php (1)

20-24: LGTM!

The sanitization properly prevents log injection by:

  • Using basename() to avoid path disclosure
  • Removing control characters
  • Limiting length to 255 characters

The generic exit message avoids information leakage.

src/OpenCoreEMR/FrontController/SecurityValidator.php (4)

25-28: LGTM!

Strict CLI detection using php_sapi_name() === 'cli' is the correct approach.


33-36: LGTM!

The regex correctly matches both .inc and .inc.php files (case-insensitive), addressing the security requirement.


41-44: LGTM!

Centralized security logging with a consistent prefix is good practice.


49-52: LGTM!

The environment variable name OPENEMR_FC_LOG_LEVEL uses the correct single underscore convention, matching the documented pattern.

src/OpenCoreEMR/FrontController/Router.php (6)

27-35: LGTM!

The constructor properly validates that realpath($baseDir) succeeds before proceeding, throwing a clear exception if the directory is invalid. This addresses the previous review concern.


40-49: LGTM!

Route extraction using strtok() correctly strips the query string. Validation happens in later methods.


54-61: LGTM!

Multisite detection with a sensible default and explicit parameter setting for downstream compatibility.


83-86: LGTM!

Clean delegation to configuration with appropriate OR logic for deprecated and forbidden checks.


91-94: LGTM!

Clean delegation to configuration for admin path detection.


99-119: LGTM!

Robust target file resolution with:

  • realpath() for path normalization
  • Path traversal prevention using str_starts_with() with DIRECTORY_SEPARATOR
  • File existence and extension validation

This addresses previous review concerns about path traversal security.

phpunit.xml (1)

30-30: LGTM!

The FrontController test suite is properly configured, following the same pattern as existing test suites. It's included in both the main openemr suite and as a standalone suite for selective execution.

Also applies to: 61-63

home.php (7)

23-27: LGTM!

Standard dependency loading pattern. Using safeLoad() is appropriate to avoid overriding existing environment variables.


30-30: LGTM!

The environment variable name OPENEMR_ENABLE_FRONT_CONTROLLER uses the correct single underscore, matching the documented convention.


30-77: LGTM with minor observation.

Pass-through mode correctly:

  • Blocks .inc and .inc.php files
  • Checks for path traversal (.. and null bytes)
  • Only routes .php files
  • Updates server variables for correct execution context

Note: The path traversal check at line 35 uses string matching. While effective for basic cases, realpath() validation (used in the main flow) is more robust. Consider adding it here if not relying on Apache/Nginx to filter these already.


79-85: LGTM!

Good extension point for modules with defensive checks for kernel existence.


88-108: LGTM!

Clean routing setup with proper separation of concerns:

  • Multisite detection
  • Trailing slash handling
  • Forbidden path checks
  • Admin flag propagation via $_SERVER

110-119: LGTM!

Consistent .inc/.inc.php blocking with:

  • Centralized validation via SecurityValidator
  • Appropriate 403 response
  • Security event logging
  • Conditional debug message detail

122-163: LGTM!

Robust error handling with:

  • Null safety checks
  • Shutdown function to ensure late events fire even on early exit
  • Try-catch with logging and re-throw to preserve normal error handling
  • Context propagation for observability

The error handling architecture is well-designed for production use.

tests/Tests/FrontController/SecurityTest.php (5)

59-74: Test logic is sound but depends on loadVulnerableIncFiles fix.

Once the base path issue in loadVulnerableIncFiles is resolved, this test will correctly validate that .inc.php files return 403.


79-92: Test logic correctly handles both Apache and nginx responses.

The dual acceptance of 403 (blocked) or 500 (execution failure) appropriately handles different web server configurations.


124-189: File validation tests are well-structured.

The tests for non-PHP files, non-existent files, and legitimate PHP file access correctly validate the front controller's security boundaries.


198-210: Appropriate skip with clear rationale.

Skipping this test is correct since environment variable control requires infrastructure not available in the test environment. The comment provides clear guidance for manual testing.


215-232: Report generation logic is correct.

The security report generation properly handles directory creation and JSON formatting. This provides valuable audit artifacts.

tests/Tests/FrontController/CompatibilityTest.php (4)

29-40: Setup is consistent with SecurityTest.

The base URL resolution and HTTP client configuration follow the same pattern as SecurityTest.php, ensuring consistent test behavior across the suite.


42-197: Core compatibility tests are well-designed.

The test methods for index access, login, multisite, API routing, portal access, OAuth2, static assets, and patient workflows provide comprehensive coverage of backward compatibility requirements.


240-254: Custom module accessibility check is appropriate.

Testing that custom modules in sites/default/custom/ are not blocked (expecting 404 or 200, but not 403) correctly validates the routing rules.


281-318: File upload and report generation tests are sound.

The test for file upload paths correctly expects authentication checks rather than outright blocking. The compatibility report generation follows the same good pattern as SecurityTest.php.

Comment on lines +835 to 840
- name: Upload FrontController test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}

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

Add missing parameters to Codecov upload.

The FrontController test results upload is missing critical parameters that all other test uploads include:

  1. files parameter should specify the junit file (e.g., junit-frontcontroller.xml)
  2. flags parameter should categorize results (e.g., frontcontroller,php${{ steps.parse.outputs.php }},...)

Apply this diff to align with other test uploads:

     - name: Upload FrontController test results to Codecov
       if: ${{ !cancelled() }}
       uses: codecov/test-results-action@v1
       with:
         token: ${{ secrets.CODECOV_TOKEN }}
+        files: junit-frontcontroller.xml
+        flags: frontcontroller,php${{ steps.parse.outputs.php }},${{ steps.parse.outputs.webserver }},${{ steps.parse.outputs.database }}${{ steps.parse.outputs.db }}
📝 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
- name: Upload FrontController test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload FrontController test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: junit-frontcontroller.xml
flags: frontcontroller,php${{ steps.parse.outputs.php }},${{ steps.parse.outputs.webserver }},${{ steps.parse.outputs.database }}${{ steps.parse.outputs.db }}
🤖 Prompt for AI Agents
.github/workflows/test.yml around lines 835 to 840: the Codecov step is missing
the files and flags parameters; update this step to include files: the junit
file for FrontController (e.g., junit-frontcontroller.xml) and flags: a
comma-separated set that includes at least frontcontroller and a PHP-version tag
using the parsed output (e.g., frontcontroller,php${{ steps.parse.outputs.php
}}); ensure the flags format matches other test uploads in the workflow so
results are categorized consistently.

Comment on lines +10 to +13
```
interface/patient_file/history/history.inc.php
Fatal error: Call to undefined function xl()
```
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

Add language identifier to code block.

The fenced code block at line 10 should specify a language (e.g., text or log) for proper syntax highlighting.

Apply this diff:

-```
+```text
 interface/patient_file/history/history.inc.php
 Fatal error: Call to undefined function xl()

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

10-10: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In README-FRONT-CONTROLLER.md around lines 10 to 13, the fenced code block is
missing a language identifier; update the opening fence from totext so
the block becomes a text/log block (i.e., replace the existing opening triple
backticks with ```text and leave the inner lines unchanged), ensuring proper
syntax highlighting.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

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: 0

♻️ Duplicate comments (7)
tests/scripts/test_compatibility.sh (4)

227-228: Inconsistent test counting—delegate to run_test helper.

The manual TOTAL_TESTS increment duplicates logic already in run_test(). Consider refactoring this POST test block to use the helper function for consistency.


25-34: Let tput failures fall back instead of aborting.

With set -e, any non-zero tput (common when TERM lacks color support) terminates the suite before tests run. Swallow those failures and fall back to plain text.

 # Colors for output using tput (with fallback for non-interactive environments)
-if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
+if command -v tput >/dev/null 2>&1 && [[ -n "${TERM:-}" ]]; then
+    RED=$(tput setaf 1 2>/dev/null || printf '')
+    GREEN=$(tput setaf 2 2>/dev/null || printf '')
+    NC=$(tput sgr0 2>/dev/null || printf '')
+    if [[ -z "${RED}" || -z "${GREEN}" || -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
 else
     RED=""
     GREEN=""
     NC=""
 fi

54-79: Fix status-code matching to avoid false positives.

[[ "${expected_codes}" = *"${http_code}"* ]] treats 000 and other garbage codes as success because they are substrings of 200, so we miss real outages.

-    # Check if code matches any of the expected codes
-    if [[ "${expected_codes}" = *"${http_code}"* ]]; then
+    # Check if code matches any of the expected codes (as whole token, not substring)
+    if [[ " ${expected_codes} " == *" ${http_code} "* ]]; then

234-242: Fail POST compatibility tests on 5xx/connection errors.

Any 5xx (or 000 curl error) currently lands in the "PASS" branch, so a broken login POST can still count as success. Tighten the check so unexpected 3-digit codes and connection errors fail the test.

-if [[ "${http_code}" = "404" || "${http_code}" = "403" ]]; then
+if [[ "${http_code}" = "404" || "${http_code}" = "403" || "${http_code}" = "000" || "${http_code}" =~ ^5 ]]; then
     printf "%sFAIL%s (HTTP %s)\n" "${RED}" "${NC}" "${http_code}" | tee -a "${REPORT_FILE}"
     FAILED_TESTS=$((FAILED_TESTS + 1))
     echo "  ✗ POST requests are being blocked" | tee -a "${REPORT_FILE}"
 else
tests/scripts/test_security.sh (1)

15-17: Add set -o pipefail to honor pipeline failures.

Line 15 has set -e, but the script uses pipes extensively (e.g., | tee). Without set -o pipefail, errors in the piped command are masked. ShellCheck flags this.

Apply this diff:

 set -e
+set -o pipefail
 # Ensure errexit is inherited in command substitutions (addresses SC2311)
 shopt -s inherit_errexit 2>/dev/null || true

Based on pipeline failures

tests/scripts/test_performance.sh (2)

52-60: Add missing bc dependency check (critical blocker).

The script uses bc for floating-point arithmetic starting at line 67 (e.g., bc -l <<< "${overhead} < 5"), but there is no check to ensure bc is available. If bc is not installed, the script will fail with a cryptic error at runtime. Add a dependency check alongside the ab check using the same pattern.

 # Check if Apache Bench (ab) is available
 if ! command -v ab &> /dev/null; then
     {
         printf "%sERROR: Apache Bench (ab) not available. Please install it manually.%s\n" "${RED}" "${NC}"
         echo "  macOS: brew install httpd"
         echo "  Linux: sudo apt-get install apache2-utils"
     } | tee -a "${REPORT_FILE}"
     exit 1
 fi
+
+# Check if bc (basic calculator) is available for floating-point arithmetic
+if ! command -v bc &> /dev/null; then
+    {
+        printf "%sERROR: bc (basic calculator) not available. Please install it.%s\n" "${RED}" "${NC}"
+        echo "  macOS: brew install bc"
+        echo "  Linux: sudo apt-get install bc"
+    } | tee -a "${REPORT_FILE}"
+    exit 1
+fi

120-128: Validate ab output format before parsing metrics.

The metric extraction uses || echo "0" fallback when awk fails, which masks parsing errors and distinguishes poorly between legitimate zero values and extraction failures. Previous reviews flagged this; the validation on lines 243–248 occurs too late (after metrics are already captured). Validate the ab output contains expected sections immediately after the ab command succeeds, before extracting metrics.

     # Run Apache Bench test
     if ! ab_output=$(ab -n "${ITERATIONS}" -c 10 -q "${url}" 2>&1); then
         {
             printf "%sERROR: Apache Bench failed for %s%s\n" "${RED}" "${url}" "${NC}"
             echo "Output: ${ab_output}"
         } | tee -a "${REPORT_FILE}"
         return 1
     fi
+
+    # Validate ab output contains expected sections
+    if ! printf '%s\n' "$ab_output" | grep -q "Requests per second"; then
+        {
+            printf "%sWARNING: Unexpected ab output format (missing 'Requests per second' section)%s\n" "${RED}" "${NC}"
+        } | tee -a "${REPORT_FILE}"
+        return 1
+    fi
 
     # Extract key metrics using awk with safe defaults
-    requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "${ab_output}" || echo "0")
-    time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "${ab_output}" || echo "0")
-    failed_requests=$(awk '/Failed requests/ {print $3}' <<< "${ab_output}" || echo "0")
+    requests_per_sec=$(printf '%s\n' "$ab_output" | awk '/Requests per second/ {print $4}')
+    time_per_request=$(printf '%s\n' "$ab_output" | awk '/Time per request.*mean\)/ {print $4; exit}')
+    failed_requests=$(printf '%s\n' "$ab_output" | awk '/Failed requests/ {print $3}')
 
     # Extract percentiles using awk with safe defaults
-    p50=$(awk '/50%/ {print $2}' <<< "${ab_output}" || echo "0")
-    p95=$(awk '/95%/ {print $2}' <<< "${ab_output}" || echo "0")
-    p99=$(awk '/99%/ {print $2}' <<< "${ab_output}" || echo "0")
+    p50=$(printf '%s\n' "$ab_output" | awk '/50%/ {print $2}')
+    p95=$(printf '%s\n' "$ab_output" | awk '/95%/ {print $2}')
+    p99=$(printf '%s\n' "$ab_output" | awk '/99%/ {print $2}')
🧹 Nitpick comments (3)
tests/scripts/test_security.sh (1)

24-33: Suppress tput errors to prevent abort under set -e in CI environments.

The current check for tput and TERM is reasonable, but in CI (e.g., TERM=dumb) tput can still fail with exit code 1, causing the script to abort despite the fallback. Suppress individual tput errors so the script gracefully degrades to plain output.

Apply this diff:

 if command -v tput >/dev/null 2>&1 && [[ -n "${TERM}" ]]; then
-    RED=$(tput setaf 1)
-    GREEN=$(tput setaf 2)
-    NC=$(tput sgr0) # No Color
+    RED=$(tput setaf 1 2>/dev/null || true)
+    GREEN=$(tput setaf 2 2>/dev/null || true)
+    NC=$(tput sgr0 2>/dev/null || true)
+    # Fall back to plain text if any tput call failed (returned empty)
+    if [[ -z "${RED}" ]] || [[ -z "${GREEN}" ]] || [[ -z "${NC}" ]]; then
+        RED=""; GREEN=""; NC=""
+    fi
 else
     RED=""
     GREEN=""
     NC=""
 fi
tests/scripts/test_performance.sh (2)

16-18: Use portable error handling instead of bash-specific inherit_errexit.

Line 18 uses shopt -s inherit_errexit, which is bash-specific and not available in POSIX sh or dash. While the fallback || true prevents script failure, it silently disables the fix on non-bash systems, reintroducing the SC2311 issues (command substitutions not inheriting set -e). A more portable approach is to explicitly check return codes on critical command assignments (e.g., using if ! var=$(cmd); then exit 1; fi), similar to the existing pattern in run_performance_test at lines 112–118.

For critical calls like lines 154–157, 171–174, 188–191, 206–209, and 227–230, explicitly guard them:

-baseline=$(run_performance_test \
-    "Direct index.php access" \
-    "${BASE_URL}/index.php" \
-    "Direct file access without front controller")
+if ! baseline=$(run_performance_test \
+    "Direct index.php access" \
+    "${BASE_URL}/index.php" \
+    "Direct file access without front controller"); then
+    {
+        printf "%sERROR: Baseline performance test failed%s\n" "${RED}" "${NC}"
+    } | tee -a "${REPORT_FILE}"
+    exit 1
+fi

(Apply the same pattern to all other run_performance_test assignments.)


243-248: Improve regex to handle all numeric formats (optional refinement).

The numeric validation regex on line 245 (^[0-9]+\.?[0-9]*$) does not match negative numbers (e.g., -5.2). Although performance metrics are typically non-negative, consider using a more inclusive pattern for robustness: ^-?[0-9]+\.?[0-9]*$.

-    if ! [[ "${baseline_time}" =~ ^[0-9]+\.?[0-9]*$ ]] || ! [[ "${fc_time}" =~ ^[0-9]+\.?[0-9]*$ ]]; then
+    if ! [[ "${baseline_time}" =~ ^-?[0-9]+\.?[0-9]*$ ]] || ! [[ "${fc_time}" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64cc024 and f80a50d.

📒 Files selected for processing (4)
  • security-check.php (1 hunks)
  • tests/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • security-check.php
🧰 Additional context used
🧬 Code graph analysis (2)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (52-77)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (54-79)
🔇 Additional comments (2)
tests/scripts/test_security.sh (2)

52-77: run_test function implementation looks good.

The helper properly captures HTTP status codes, compares against expected values, and logs results to both terminal and report file. Variable bracing and error handling are correct.


79-251: Test categories and report structure are well-organized.

The six test categories are clearly delineated with headers, each test case has a descriptive message, and the final report properly summarizes results. The feature flag test (lines 211–224) correctly accepts both 200 and 302 responses. Exit codes align with test outcomes.

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.

1 participant