Skip to content

8964 global front controller#1

Closed
coding-sunshine wants to merge 11 commits intomasterfrom
8964-global-front-controller
Closed

8964 global front controller#1
coding-sunshine wants to merge 11 commits intomasterfrom
8964-global-front-controller

Conversation

@coding-sunshine
Copy link
Copy Markdown
Owner

@coding-sunshine coding-sunshine commented Oct 3, 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 for centralized request routing (feature-flagged).
    • Webserver rules to route requests and serve static assets while protecting include files.
  • Security

    • Blocks direct access to include files, prevents path traversal, and logs security events.
    • Adds an opt-in runtime guard to deny include-file requests.
  • Configuration

    • New environment variables to enable the front controller and control logging.
  • Documentation

    • Comprehensive front controller guide with setup, troubleshooting, and rollback steps.
  • Tests / CI

    • Compatibility, security, and performance test suites plus CI steps to run and upload results; test reports generated and ignored by VCS.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 3, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a global front controller: new home.php, Router, RouteConfig(+interface), SecurityValidator, security-check bootstrap, .htaccess rules, tests, CI steps, docs, environment flags, composer PSR-4 namespace, and several test scripts and reports to validate routing, security, and performance.

Changes

Cohort / File(s) Summary
Front Controller Core
home.php, src/OpenCoreEMR/FrontController/Router.php, src/OpenCoreEMR/FrontController/RouteConfig.php, src/OpenCoreEMR/FrontController/SecurityValidator.php, src/OpenCoreEMR/FrontController/RouteConfigInterface.php
Adds centralized front controller runtime (home.php), routing resolver (Router), configurable route rules (RouteConfig + interface), and static security utilities (SecurityValidator). Implements route extraction, multisite handling, forbidden/admin/deprecated checks, target resolution, and safety helpers.
Event System
src/Events/FrontController/FrontControllerEvent.php
Adds FrontControllerEvent to expose early/late lifecycle events with route, siteId, and context getters/setters for extension hooks.
Web Server & PHP Bootstrap
.htaccess, security-check.php
Adds Apache rewrite/pass-through rules and static-asset allowances; introduces PHP-level pre-execution guard to block direct access to .inc.php files.
Configuration & Docs
.env.example, README-FRONT-CONTROLLER.md, composer.json
Adds shebang and two env vars (OPENEMR_ENABLE_FRONT_CONTROLLER, OPENEMR_FC_LOG_LEVEL) to .env.example; comprehensive front-controller README; registers OpenCoreEMR\ PSR-4 namespace in composer.json.
CI & Test Config
.github/workflows/test.yml, phpunit.xml
Adds CI steps for FrontController tests and Codecov upload; registers a dedicated PHPUnit frontcontroller test-suite and discovery.
PHPUnit Tests
tests/Tests/FrontController/CompatibilityTest.php, tests/Tests/FrontController/SecurityTest.php
Adds HTTP-level compatibility and security test classes using Guzzle, asserting routing behavior, asset pass-through, blocking rules, headers, multisite, and producing JSON reports.
Test Scripts (Bash)
tests/scripts/test_compatibility.sh, tests/scripts/test_security.sh, tests/scripts/test_performance.sh
Adds curl/ab-based scripts to run compatibility, security, and performance checks and generate timestamped reports and exit codes.
Test Reports & Placeholders
tests/reports/compatibility-test-report.json, tests/reports/security-test-report.json, reports/.gitkeep
Adds sample/generated JSON reports and a .gitkeep to preserve reports directories; updates .gitignore to ignore tests/reports/.
Misc CI/Test Helpers
.gitignore
Adds tests/reports/ to ignore patterns.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant A as Apache (.htaccess)
  participant H as home.php (Front Controller)
  participant S as security-check.php
  participant V as SecurityValidator
  participant R as Router
  participant E as Event Dispatcher
  participant T as Target PHP

  C->>A: HTTP Request
  A->>A: Check static/allowed paths
  alt Pass-through/static
    A-->>C: Serve resource or existing front controller
  else Routed to front controller
    A->>H: Rewrite to home.php with _ROUTE
    H->>S: Pre-exec guard (block .inc.php)
    H->>V: Early validation/logging
    H->>R: Init Router (baseDir + RouteConfig)
    R->>R: extractRoute(), determineSiteId(), handleTrailingSlash()
    R->>R: isForbidden()/requiresAdmin()
    H->>E: Dispatch front_controller.early
    R->>R: resolveTargetFile()
    H->>T: include target file (try/catch)
    H->>E: Dispatch front_controller.late (on shutdown/exception)
    T-->>H: Output
    H-->>C: Response
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant H as home.php
  Note over H: Feature flag: OPENEMR_ENABLE_FRONT_CONTROLLER
  C->>H: Request rewritten to home.php
  alt Feature flag disabled
    H-->>C: Transparent passthrough to requested script (with safety checks)
  else Feature flag enabled
    H->>R: Full Router-driven front controller flow (see main diagram)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hop through routes with careful nose,
Guarding .inc snug where no one goes.
Logs like carrots line the trail,
Tests and CI help every detail.
Home.php hums — safe paths I choose. 🐇✨

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 title concisely identifies the introduction of a global front controller, which is the primary change in the pull request; however, the leading "8964" appears to be an issue or branch reference that does not add meaningful context.
Docstring Coverage ✅ Passed Docstring coverage is 85.19% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (16)
  • .env.example (1 hunks)
  • .gitignore (1 hunks)
  • .htaccess (1 hunks)
  • README-FRONT-CONTROLLER.md (1 hunks)
  • home.php (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/scripts/test_compatibility.sh (1 hunks)
  • tests/scripts/test_performance.sh (1 hunks)
  • tests/scripts/test_security.sh (1 hunks)

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

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

1-40: Setup is mostly sound, but error handling needs refinement.

The script header, AI disclosure, and configuration setup are clear and appropriate. However:

  1. Line 16 (set -e): While this ensures the script exits on errors, it can cause unexpected exits with command substitutions. Several commands later in the script use || true to avoid this, but the pattern is inconsistent.

  2. Lines 19-20: No validation of BASE_URL format. If an invalid URL is provided, tests will fail silently or produce misleading results.

Consider adding input validation:

 # Configuration
 BASE_URL="${1:-http://localhost/openemr}"
+
+# Validate BASE_URL format
+if ! [[ "$BASE_URL" =~ ^https?:// ]]; then
+    echo "${RED}ERROR: Invalid BASE_URL format. Must start with http:// or https://${NC}" >&2
+    exit 1
+fi
+
 ITERATIONS="${2:-100}"

For set -e, consider being more explicit about which commands should ignore errors:

-set -e
+set -euo pipefail
+# Note: Individual commands that may fail use || true explicitly

95-182: Improve static asset test and add connectivity check

  • Replace file-existence check with HTTP HEAD for static assets to ensure URL accessibility:
- if [[ -d "$(dirname "$0")/../../public/assets" ]]; then
+ if curl -sf --head "$BASE_URL/public/assets/css/style.css" > /dev/null 2>&1; then
     static_result=$(run_performance_test \
         "Static CSS file" \
         "$BASE_URL/public/assets/css/style.css" \
         "Static assets should bypass front controller")
     static_rps=$(cut -d'|' -f1 <<< "$static_result")
 else
     echo "  ℹ Skipping static asset tests (static asset URL not accessible)" | tee -a "$REPORT_FILE"
 fi
  • Add an initial connectivity check after creating REPORT_FILE to fail fast if BASE_URL is unreachable:
 } | tee "$REPORT_FILE"

+# Verify BASE_URL is reachable
+if ! curl -sf --head "$BASE_URL/index.php" > /dev/null 2>&1; then
+    echo "ERROR: Cannot connect to BASE_URL: $BASE_URL" | tee -a "$REPORT_FILE"
+    exit 1
+fi

 # Check if Apache Bench (ab) is available
tests/reports/compatibility-test-report.json (1)

1-5: LGTM!

The report structure is clear and the data is well-formed. The escaped forward slashes in the URL (\/) are valid JSON but unnecessary in modern parsers—unescaped slashes work fine and improve readability.

composer.json (1)

189-192: Consider simplifying the autoload configuration.

The new "OpenCoreEMR\\": "src/OpenCoreEMR" mapping is valid, but technically redundant since "OpenEMR\\": "src" already covers all namespaces under src/. This means classes in src/OpenCoreEMR are already autoloadable via the existing mapping.

Having both mappings works fine and may provide clarity or facilitate future namespace separation. If the intention is to keep both for organizational reasons, this is acceptable.

tests/scripts/test_security.sh (2)

43-68: Improve error handling in the run_test function.

The function uses curl -s -o /dev/null -w "%{http_code}" -L which may hide connection errors. If curl fails to connect, it may return an empty string or "000", which won't match expected codes and will be reported as a test failure rather than a connection error.

Apply this diff to add explicit error handling:

 run_test() {
     local test_name="$1"
     local url="$2"
     local expected_code="$3"
     local description="$4"
 
     TOTAL_TESTS=$((TOTAL_TESTS + 1))
     printf "Testing: %s... " "$test_name" | tee -a "$REPORT_FILE"
 
     # 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 "$url" 2>&1)
+    
+    # Check for curl errors (empty or 000 status code)
+    if [[ -z "$http_code" || "$http_code" == "000" ]]; then
+        printf "%sERROR%s (Connection failed)\n" "$RED" "$NC" | tee -a "$REPORT_FILE"
+        FAILED_TESTS=$((FAILED_TESTS + 1))
+        {
+            echo "  ✗ Unable to connect to server"
+            echo "  URL: $url"
+        } | tee -a "$REPORT_FILE"
+        echo | tee -a "$REPORT_FILE"
+        return
+    fi
 
     if [[ "$http_code" = "$expected_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_code" "$http_code" | tee -a "$REPORT_FILE"
         FAILED_TESTS=$((FAILED_TESTS + 1))
         {
             echo "  ✗ $description"
             echo "  URL: $url"
         } | tee -a "$REPORT_FILE"
     fi
     echo | tee -a "$REPORT_FILE"
 }

194-218: Strengthen the security headers test.

The current test uses curl -s -I and checks if headers contain the security header names, but doesn't validate the header values. This could pass even if the headers are present but set to insecure values.

Apply this diff to validate header values:

 TOTAL_TESTS=$((TOTAL_TESTS + 1))
 printf "Testing: Security headers... " | tee -a "$REPORT_FILE"
 
 headers=$(curl -s -I "$BASE_URL/index.php")
 
-if echo "$headers" | grep -q "X-Content-Type-Options" && \
-   echo "$headers" | grep -q "X-XSS-Protection" && \
-   echo "$headers" | grep -q "X-Frame-Options"; then
+if echo "$headers" | grep -q "X-Content-Type-Options: nosniff" && \
+   echo "$headers" | grep -q "X-XSS-Protection: 1" && \
+   echo "$headers" | grep -q "X-Frame-Options: SAMEORIGIN"; then
     printf "%sPASS%s\n" "$GREEN" "$NC" | tee -a "$REPORT_FILE"
     PASSED_TESTS=$((PASSED_TESTS + 1))
     echo "  ✓ All required security headers present" | tee -a "$REPORT_FILE"
 else
     printf "%sFAIL%s\n" "$RED" "$NC" | tee -a "$REPORT_FILE"
     FAILED_TESTS=$((FAILED_TESTS + 1))
     echo "  ✗ Missing required security headers" | tee -a "$REPORT_FILE"
 fi
tests/scripts/test_compatibility.sh (1)

44-70: Apply the same error handling improvement as in test_security.sh.

The run_test function here has the same curl error handling issue. Connection failures should be explicitly detected and reported.

Apply a similar diff as suggested for test_security.sh:

 run_test() {
     local test_name="$1"
     local url="$2"
     local expected_codes="$3"
     local description="$4"
 
     TOTAL_TESTS=$((TOTAL_TESTS + 1))
 
     printf "Testing: %s... " "$test_name" | tee -a "$REPORT_FILE"
 
     # 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 "$url" 2>&1)
+    
+    # Check for curl errors
+    if [[ -z "$http_code" || "$http_code" == "000" ]]; then
+        printf "%sERROR%s (Connection failed)\n" "$RED" "$NC" | tee -a "$REPORT_FILE"
+        FAILED_TESTS=$((FAILED_TESTS + 1))
+        echo "  ✗ Unable to connect to server" | tee -a "$REPORT_FILE"
+        echo "  URL: $url" | tee -a "$REPORT_FILE"
+        echo | tee -a "$REPORT_FILE"
+        return
+    fi
 
     # 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"
     fi
     echo | tee -a "$REPORT_FILE"
 }
src/OpenCoreEMR/FrontController/RouteConfig.php (2)

51-70: Consider regex validation and pattern maintenance.

The default patterns look reasonable, but consider these improvements:

  1. Regex validation: The patterns are not validated at initialization. Invalid regex patterns will only fail at runtime during preg_match() calls, potentially causing security bypasses.

  2. Admin pattern readability (lines 61-62): The concatenated regex with many alternatives is hard to maintain. Consider extracting to an array and joining with |, or documenting each alternative.

  3. Document access blocking (line 56): The pattern #^sites/[^/]+/documents/# blocks all direct document access. Verify this doesn't prevent legitimate document viewing functionality that may need controlled access.

Consider adding regex validation in the add*Pattern methods:

 public function addForbiddenPattern(string $pattern): void
 {
+    if (@preg_match($pattern, '') === false) {
+        throw new \InvalidArgumentException("Invalid regex pattern: {$pattern}");
+    }
     $this->forbiddenPatterns[] = $pattern;
 }

Apply similar validation to addAdminPattern() and addDeprecatedPath().


77-100: Add duplicate pattern prevention.

The add methods allow duplicate patterns to be added, which could cause unnecessary performance overhead during pattern matching.

Consider checking for duplicates before adding:

 public function addForbiddenPattern(string $pattern): void
 {
+    if (in_array($pattern, $this->forbiddenPatterns, true)) {
+        return;
+    }
     $this->forbiddenPatterns[] = $pattern;
 }

Apply similar logic to addAdminPattern() and addDeprecatedPath().

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6666e36 and b23f6e0.

📒 Files selected for processing (19)
  • .env.example (1 hunks)
  • .github/workflows/test.yml (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/OpenCoreEMR/FrontController/RouteConfig.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 (8)
security-check.php (1)
library/classes/thumbnail/ThumbnailGenerator.php (1)
  • error_log (212-216)
home.php (3)
src/OpenCoreEMR/FrontController/Router.php (7)
  • Router (20-141)
  • determineSiteId (52-63)
  • extractRoute (36-47)
  • handleTrailingSlash (70-77)
  • isForbiddenPath (82-91)
  • requiresAdmin (96-99)
  • resolveTargetFile (104-124)
src/OpenCoreEMR/FrontController/SecurityValidator.php (4)
  • SecurityValidator (20-53)
  • logSecurityEvent (41-44)
  • isIncludeFile (33-36)
  • getDebugMessage (49-52)
src/OpenCoreEMR/FrontController/RouteConfig.php (1)
  • requiresAdmin (124-132)
tests/Tests/FrontController/CompatibilityTest.php (1)
tests/Tests/FrontController/SecurityTest.php (1)
  • setUpBeforeClass (30-39)
tests/scripts/test_security.sh (1)
tests/scripts/test_compatibility.sh (1)
  • run_test (45-70)
tests/scripts/test_compatibility.sh (1)
tests/scripts/test_security.sh (1)
  • run_test (43-68)
src/OpenCoreEMR/FrontController/Router.php (1)
src/OpenCoreEMR/FrontController/RouteConfig.php (5)
  • RouteConfig (20-221)
  • __construct (43-46)
  • isDeprecated (140-148)
  • isForbidden (108-116)
  • requiresAdmin (124-132)
src/OpenCoreEMR/FrontController/RouteConfig.php (1)
src/OpenCoreEMR/FrontController/Router.php (2)
  • __construct (27-31)
  • requiresAdmin (96-99)
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)


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

(MD040, fenced-code-language)

🔇 Additional comments (40)
.env.example (1)

5-11: LGTM!

The environment variable additions are well-documented and follow existing conventions. The comments clearly explain the purpose, valid values, and default behavior (disabled by default).

security-check.php (2)

1-14: LGTM!

The file header is well-documented with appropriate copyright, license, and usage information.


15-15: Request path detection is acceptable but could be more defensive.

The current implementation using SCRIPT_FILENAME and PHP_SELF is functional, but consider these points:

  • SCRIPT_FILENAME can be affected by server configuration (e.g., CGI/FastCGI setups)
  • PHP_SELF can contain user input in some configurations
  • If both are unavailable, the empty string fallback will cause the regex check to pass (which is safe, but the intent isn't explicit)

The current logic is safe for the regex match use case, but for defense-in-depth, consider logging when both variables are unavailable to detect configuration issues.

tests/scripts/test_performance.sh (1)

42-50: LGTM!

The Apache Bench validation is well-implemented with clear error messaging and platform-specific installation instructions.

.github/workflows/test.yml (1)

344-354: LGTM build_test() in ci/ciLibrary.source accepts the “frontcontroller” testsuite as expected.

reports/.gitkeep (1)

1-1: No code issues to report.

Empty placeholder file is fine for preserving the directory.

phpunit.xml (2)

30-30: LGTM!

The FrontController test directory is correctly added to the main test suite for comprehensive coverage.


59-61: LGTM!

The dedicated frontcontroller test suite enables isolated testing of front controller functionality via --testsuite frontcontroller, which is useful for focused test runs during development and CI.

tests/scripts/test_security.sh (8)

1-14: LGTM!

Clear script header with usage instructions and AI disclosure. The licensing and copyright information is properly documented.


15-42: LGTM!

Configuration and initialization are clean. The script properly sets up directories, colors using tput for portability, and initializes test counters.


70-95: LGTM!

The test cases for .inc.php file blocking are well-structured and cover multiple file types (.inc.php and .inc). The expected 403 responses are correct for security-sensitive include files.


96-127: LGTM!

Path traversal prevention tests are comprehensive, covering basic traversal, URL-encoded, double-encoded, and backslash variants. The expected 404 responses are appropriate.


128-173: LGTM!

Non-PHP file blocking and non-existent file handling tests are appropriate. The checks for .htaccess, composer.json, and .env files returning 404 are correct security measures.


174-193: LGTM!

Legitimate file access tests verify that the front controller doesn't break normal functionality. Testing index.php and login.php for 200 responses is appropriate.


220-242: LGTM!

The feature flag test appropriately checks that the front controller responds when enabled. The informational message when disabled (404) is helpful.


243-268: LGTM!

The final report summary is clear and provides actionable feedback. The exit codes (0 for success, 1 for failure) are correct for shell script conventions.

tests/scripts/test_compatibility.sh (7)

1-16: LGTM!

Script header is clear with proper documentation and AI disclosure.


17-43: LGTM!

Configuration and initialization are properly set up with portable color handling via tput.


72-95: LGTM!

Core entry point tests are well-defined with appropriate expected status codes (200 or 302 redirects).


96-161: LGTM!

Tests for existing front controllers, multisite support, and core workflows are comprehensive. The expected status codes appropriately allow for both successful responses and authentication redirects.


162-184: LGTM!

Static asset tests appropriately check that CSS and JS files can be served directly. The conditional skip when the public directory doesn't exist is a good fallback.


186-220: LGTM!

Custom modules and POST request tests verify that the front controller doesn't break existing functionality. The explicit check for 403/404 blocking on POST requests is valuable.


222-273: LGTM!

File upload and AJAX endpoint tests ensure backward compatibility. The final report summary is clear and actionable.

.htaccess (7)

1-6: LGTM!

The Apache rewrite configuration is properly initialized with RewriteEngine On and RewriteBase /.


7-12: LGTM!

Forbidden path rules correctly deny access to framework internals and patient documents with 403 Forbidden responses.


14-40: LGTM!

The pass-through rules for existing front controllers preserve backward compatibility by allowing direct access to APIs, portals, OAuth2, and various interface modules. This ensures the new front controller doesn't break existing routing.


42-59: LGTM!

Static asset pass-through rules are comprehensive, covering stylesheets, scripts, images, fonts, and document formats. This ensures optimal performance by serving static files directly without PHP processing.


61-63: LGTM!

Blocking .inc.php files at the Apache level is a critical security measure. The rule is correctly placed before the physical file check to prevent direct access to include files.


65-70: LGTM!

The pass-through rules for physical files and directories ensure that existing files are served directly when they exist on disk, maintaining normal behavior for standard resources.


81-86: LGTM!

Security headers are properly configured to enhance protection against common web vulnerabilities:

  • X-Content-Type-Options: nosniff prevents MIME-sniffing
  • X-XSS-Protection: 1; mode=block enables XSS filtering
  • X-Frame-Options: SAMEORIGIN prevents clickjacking
README-FRONT-CONTROLLER.md (8)

1-25: LGTM!

The overview section clearly explains the problem (direct .inc.php access causing errors) and the solution (centralized front controller). The benefits are well-articulated with checkmarks for visual clarity.


80-116: LGTM!

Security features are comprehensively documented with clear categorization of path restrictions, admin-only paths, and the multi-layer defense for .inc.php blocking. The path traversal prevention examples cover all common attack variants.


117-200: LGTM!

Static asset pass-through and extension hooks are well-documented with practical examples. The rate limiting example demonstrates proper session-based throttling, and the performance monitoring example shows good practices for observability.


201-242: LGTM!

Quick start instructions are clear and actionable with commands for Apache, Nginx, and verification steps. The security test examples are practical and demonstrate expected responses.


243-309: LGTM!

Testing section is comprehensive with automated test suites, shell scripts, CLI support, and a manual testing checklist. Rollback procedures are clearly documented with both instant disable and full removal options.


310-349: LGTM!

Files inventory, performance expectations, and compatibility notes are accurate. The <5% overhead estimate is reasonable for the routing logic, and compatibility details cover all major deployment scenarios.


350-443: LGTM!

Configuration details and troubleshooting section are thorough. The .htaccess feature flag workaround is well-explained, addressing the limitation that Apache doesn't natively support .env files. Alternative approaches for Apache virtual hosts and PHP's putenv() are documented.


444-482: LGTM!

The FAQ section comprehensively addresses common questions about code changes, performance impact, deployment scenarios, and extension mechanisms. The Docker example with docker-compose.yml is particularly helpful.

src/OpenCoreEMR/FrontController/RouteConfig.php (2)

20-46: LGTM! Clean class structure with typed properties.

The class declaration and property initialization follow best practices with typed arrays and proper encapsulation.


197-220: LGTM! Simple and correct getter methods.

The getter methods correctly return the pattern arrays. Since PHP arrays are passed by value, returning the arrays directly does not expose internal state for modification.

Comment thread .htaccess
Comment thread home.php Outdated
Comment thread README-FRONT-CONTROLLER.md
Comment thread security-check.php
Comment on lines +108 to +148
public function isForbidden(string $route): bool
{
foreach ($this->forbiddenPatterns as $pattern) {
if (preg_match($pattern, $route)) {
return true;
}
}
return false;
}

/**
* Check if route matches any admin-only pattern.
*
* @param string $route The route to check
* @return bool True if admin-only
*/
public function requiresAdmin(string $route): bool
{
foreach ($this->adminPatterns as $pattern) {
if (preg_match($pattern, $route)) {
return true;
}
}
return false;
}

/**
* Check if route is a deprecated path.
*
* @param string $route The route to check
* @return bool True if deprecated
*/
public function isDeprecated(string $route): bool
{
foreach ($this->deprecatedPaths as $pattern) {
if (preg_match($pattern, $route)) {
return true;
}
}
return false;
}
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 error handling for preg_match failures.

The pattern matching methods use preg_match() without checking for errors. If a pattern is malformed or causes an error (e.g., catastrophic backtracking), preg_match() returns false, which is treated as "no match" – potentially allowing access to forbidden routes.

Add error checking after each preg_match() call:

 public function isForbidden(string $route): bool
 {
     foreach ($this->forbiddenPatterns as $pattern) {
-        if (preg_match($pattern, $route)) {
+        $result = preg_match($pattern, $route);
+        if ($result === false) {
+            error_log("RouteConfig: Invalid pattern or preg_match error: {$pattern}");
+            return true; // Fail-safe: treat as forbidden on error
+        }
+        if ($result === 1) {
             return true;
         }
     }
     return false;
 }

Apply similar error handling to requiresAdmin() and isDeprecated(). Note the fail-safe behavior: on pattern error, treat the route as forbidden/admin-required to prevent security bypasses.

Optional refactoring: Extract the common matching logic into a private method to reduce duplication:

private function matchesAnyPattern(string $route, array $patterns): bool
{
    foreach ($patterns as $pattern) {
        $result = preg_match($pattern, $route);
        if ($result === false) {
            error_log("RouteConfig: Invalid pattern: {$pattern}");
            return true; // Fail-safe
        }
        if ($result === 1) {
            return true;
        }
    }
    return false;
}
🤖 Prompt for AI Agents
In src/OpenCoreEMR/FrontController/RouteConfig.php around lines 108 to 148, the
three pattern-checking methods call preg_match() without handling a false return
which can occur for malformed patterns or runtime errors and would be treated as
"no match" (unsafe); update isForbidden(), requiresAdmin(), and isDeprecated()
to check preg_match() result === false, log the offending pattern (e.g.,
error_log with context), and treat false as a positive match (fail-safe: return
true) while keeping result === 1 behavior returning true for a match; to remove
duplication, extract the loop and error-handling into a private helper like
matchesAnyPattern(string $route, array $patterns): bool and have the three
public methods delegate to it.

Comment on lines +52 to +93
# Function to run performance test
run_performance_test() {
local test_name="$1"
local url="$2"
local description="$3"

{
echo "----------------------------------------"
printf "%sTest: %s%s\n" "$BLUE" "$test_name" "$NC"
echo "URL: $url"
echo "Description: $description"
echo
} | tee -a "$REPORT_FILE"

# Run Apache Bench test
ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1 || true)

# Extract key metrics using awk
requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output")
time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output")
failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output")

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

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

⚠️ Potential issue | 🟠 Major

Parsing is fragile and error handling is insufficient.

The run_performance_test function has several issues:

  1. Line 67: Using || true suppresses all errors from ab, including genuine failures (e.g., connection refused, timeout). Failed tests will produce empty metrics, but the script continues without warning.

  2. Lines 70-77: The awk parsing assumes a specific output format from Apache Bench. This is fragile and will break if:

    • ab output format changes
    • The test fails and produces different output
    • Locale settings affect number formatting
  3. Line 92: Returns unquoted variables in a pipe-delimited string. If any metric is empty (due to parsing failure), downstream code will receive malformed data like ||123.

  4. Line 67: Hardcoded -c 10 (concurrency) may not be appropriate for all test scenarios, especially for measuring single-request latency.

Apply these improvements:

 # Function to run performance test
 run_performance_test() {
     local test_name="$1"
     local url="$2"
     local description="$3"
 
     {
         echo "----------------------------------------"
         printf "%sTest: %s%s\n" "$BLUE" "$test_name" "$NC"
         echo "URL: $url"
         echo "Description: $description"
         echo
     } | tee -a "$REPORT_FILE"
 
     # Run Apache Bench test
-    ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1 || true)
+    if ! ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1); then
+        {
+            printf "%sERROR: Apache Bench failed for %s%s\n" "$RED" "$test_name" "$NC"
+            echo "  URL: $url"
+            echo "  Output: $ab_output"
+        } | tee -a "$REPORT_FILE"
+        echo "0|0|0"
+        return 1
+    fi
 
     # Extract key metrics using awk
-    requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output")
-    time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output")
-    failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output")
+    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
-    p50=$(awk '/50%/ {print $2}' <<< "$ab_output")
-    p95=$(awk '/95%/ {print $2}' <<< "$ab_output")
-    p99=$(awk '/99%/ {print $2}' <<< "$ab_output")
+    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 "  Requests/sec: ${requests_per_sec:-N/A}"
+        echo "  Time/request: ${time_per_request:-N/A}ms (mean)"
+        echo "  Failed requests: ${failed_requests:-N/A}"
+        echo "  50th percentile: ${p50:-N/A}ms"
+        echo "  95th percentile: ${p95:-N/A}ms"
+        echo "  99th percentile: ${p99:-N/A}ms"
         echo
     } | tee -a "$REPORT_FILE"
 
     # Return metrics as array (for comparison)
-    echo "$requests_per_sec|$time_per_request|$p95"
+    echo "${requests_per_sec:-0}|${time_per_request:-0}|${p95:-0}"
 }
📝 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
# Function to run performance test
run_performance_test() {
local test_name="$1"
local url="$2"
local description="$3"
{
echo "----------------------------------------"
printf "%sTest: %s%s\n" "$BLUE" "$test_name" "$NC"
echo "URL: $url"
echo "Description: $description"
echo
} | tee -a "$REPORT_FILE"
# Run Apache Bench test
ab_output=$(ab -n "$ITERATIONS" -c 10 -q "$url" 2>&1 || true)
# Extract key metrics using awk
requests_per_sec=$(awk '/Requests per second/ {print $4}' <<< "$ab_output")
time_per_request=$(awk '/Time per request.*mean\)/ {print $4; exit}' <<< "$ab_output")
failed_requests=$(awk '/Failed requests/ {print $3}' <<< "$ab_output")
# Extract percentiles using awk
p50=$(awk '/50%/ {print $2}' <<< "$ab_output")
p95=$(awk '/95%/ {print $2}' <<< "$ab_output")
p99=$(awk '/99%/ {print $2}' <<< "$ab_output")
# 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"
}
# Function to run performance test
run_performance_test() {
local test_name="$1"
local url="$2"
local description="$3"
{
echo "----------------------------------------"
printf "%sTest: %s%s\n" "$BLUE" "$test_name" "$NC"
echo "URL: $url"
echo "Description: $description"
echo
} | tee -a "$REPORT_FILE"
# 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" "$test_name" "$NC"
echo " URL: $url"
echo " Output: $ab_output"
} | tee -a "$REPORT_FILE"
echo "0|0|0"
return 1
fi
# Extract key metrics using awk
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
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:-N/A}"
echo " Time/request: ${time_per_request:-N/A}ms (mean)"
echo " Failed requests: ${failed_requests:-N/A}"
echo " 50th percentile: ${p50:-N/A}ms"
echo " 95th percentile: ${p95:-N/A}ms"
echo " 99th percentile: ${p99:-N/A}ms"
echo
} | tee -a "$REPORT_FILE"
# Return metrics as array (for comparison)
echo "${requests_per_sec:-0}|${time_per_request:-0}|${p95:-0}"
}
🤖 Prompt for AI Agents
tests/scripts/test_performance.sh around lines 52 to 93: the performance runner
currently swallows ab failures, parses its output fragily, returns
unquoted/possibly empty fields, and hardcodes concurrency; update it to (1) make
concurrency configurable via a variable (e.g., CONCURRENCY defaulting to 10)
instead of hardcoding -c 10; (2) run ab capturing both stdout and stderr but do
not use "|| true" — check ab's exit status and on non-zero exit log the full
output to REPORT_FILE, mark the test as failed, and return a clearly
identifiable failure token (e.g., "FAILED") or exit non-zero; (3) make parsing
defensive: verify expected lines exist before extracting values, normalize
locale (LC_ALL=C) when calling ab/awk to prevent number-format issues, and
provide sensible defaults (e.g., "N/A" or 0) when a metric cannot be parsed; (4)
ensure the returned pipe-delimited string is quoted and fields are
escaped/normalized so empty values become explicit tokens (e.g., "N/A|N/A|N/A")
to avoid producing malformed downstream input; and (5) add concise logs to
REPORT_FILE when parsing fails so downstream consumers can detect and handle
test errors.

Comment on lines +184 to +296
{
echo "========================================"
echo "PERFORMANCE ANALYSIS"
echo "========================================"
echo
} | tee -a "$REPORT_FILE"

# Calculate overhead
if [[ -n "$baseline_time" && -n "$fc_time" ]]; then
overhead=$(awk "BEGIN {printf \"%.2f\", ($fc_time - $baseline_time) / $baseline_time * 100}")

{
echo "Front Controller Overhead:"
echo " Baseline (direct): ${baseline_time}ms"
echo " Front controller: ${fc_time}ms"
echo " Overhead: ${overhead}%"
echo
} | tee -a "$REPORT_FILE"

# Performance verdict
if (( $(echo "$overhead < 5" | bc -l) )); then
{
printf "%s✓ EXCELLENT: Overhead < 5%%%s\n" "$GREEN" "$NC"
echo " Front controller has negligible performance impact"
} | tee -a "$REPORT_FILE"
elif (( $(echo "$overhead < 10" | bc -l) )); then
{
printf "%s✓ GOOD: Overhead < 10%%%s\n" "$GREEN" "$NC"
echo " Front controller has acceptable performance impact"
} | tee -a "$REPORT_FILE"
elif (( $(echo "$overhead < 20" | bc -l) )); then
{
printf "%s⚠ ACCEPTABLE: Overhead < 20%%%s\n" "$YELLOW" "$NC"
echo " Front controller adds some overhead, consider optimization"
} | tee -a "$REPORT_FILE"
else
{
printf "%s✗ HIGH OVERHEAD: > 20%%%s\n" "$RED" "$NC"
echo " Front controller adds significant overhead, optimization needed"
} | tee -a "$REPORT_FILE"
fi
fi

echo | tee -a "$REPORT_FILE"

# Throughput comparison
{
echo "Throughput Comparison:"
echo " Baseline: ${baseline_rps} req/sec"
echo " Front controller: ${fc_rps} req/sec"
echo " Login page: ${login_rps} req/sec"
if [[ -n "$static_rps" ]]; then
echo " Static assets: ${static_rps} req/sec"
fi
echo " REST API: ${api_rps} req/sec"
echo
} | tee -a "$REPORT_FILE"

# Latency Analysis
{
echo "Latency Analysis (95th percentile):"
echo " Baseline: ${baseline_p95}ms"
echo " Front controller: ${fc_p95}ms"
echo
} | tee -a "$REPORT_FILE"

# Resource Usage (if available)
if command -v ps &> /dev/null; then
php_procs=$(ps aux | grep php | grep -v grep | wc -l)
{
echo "Current Resource Usage:"
echo " Active PHP processes: $php_procs"
} | tee -a "$REPORT_FILE"

if command -v top &> /dev/null; then
cpu_usage=$(grep "CPU usage" <<< "$(top -l 1)" | awk '{print $3}' || echo "N/A")
echo " CPU usage: $cpu_usage" | tee -a "$REPORT_FILE"
fi
echo | tee -a "$REPORT_FILE"
fi

# Recommendations
{
echo "========================================"
echo "RECOMMENDATIONS"
echo "========================================"
echo
} | tee -a "$REPORT_FILE"

if [[ -n "$overhead" ]]; then
if (( $(echo "$overhead < 10" | bc -l) )); then
{
echo "✓ Performance is acceptable for production use"
echo "✓ Front controller adds minimal overhead"
echo "✓ Security benefits outweigh performance impact"
} | tee -a "$REPORT_FILE"
else
{
echo "⚠ Consider performance optimizations:"
echo " - Enable OPcache for PHP"
echo " - Use FastCGI process manager (PHP-FPM)"
echo " - Enable static asset caching"
echo " - Consider CDN for static assets"
} | tee -a "$REPORT_FILE"
fi
fi

{
echo
echo "========================================"
echo "Report saved to: $REPORT_FILE"
echo "========================================"
} | 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 | 🟠 Major

Arithmetic operations lack error handling and portability concerns.

The analysis and reporting section has several reliability issues:

  1. Lines 192-193: Arithmetic with awk assumes baseline_time and fc_time are valid numbers. If the previous tests failed or returned "0", the overhead calculation will be meaningless or produce division-by-zero errors.

  2. Lines 204-224: Multiple comparisons using bc -l assume bc is available. While common, it's not always installed by default (e.g., minimal Docker images, some BSD systems).

  3. Line 259: The top command parsing is macOS-specific (top -l 1) and the grep pattern is fragile. This will fail on Linux and other Unix systems.

  4. Lines 235-237: Variable expansions like ${static_rps} in the throughput comparison are not quoted. If the variable is empty (because the test was skipped), the output will show an empty line.

  5. Lines 273-289: Recommendations are based solely on overhead percentage without considering absolute time (e.g., 20% overhead on 5ms is very different from 20% on 500ms).

Apply these fixes:

 # Calculate overhead
 if [[ -n "$baseline_time" && -n "$fc_time" ]]; then
-    overhead=$(awk "BEGIN {printf \"%.2f\", ($fc_time - $baseline_time) / $baseline_time * 100}")
+    # Validate numeric values and avoid division by zero
+    if [[ "$baseline_time" =~ ^[0-9]+\.?[0-9]*$ ]] && [[ "$fc_time" =~ ^[0-9]+\.?[0-9]*$ ]] && (( $(awk "BEGIN {print ($baseline_time > 0)}") )); then
+        overhead=$(awk "BEGIN {printf \"%.2f\", ($fc_time - $baseline_time) / $baseline_time * 100}")
+    else
+        overhead="N/A"
+    fi
 
     {
         echo "Front Controller Overhead:"
         echo "  Baseline (direct): ${baseline_time}ms"
         echo "  Front controller: ${fc_time}ms"
         echo "  Overhead: ${overhead}%"
         echo
     } | tee -a "$REPORT_FILE"
 
     # Performance verdict
-    if (( $(echo "$overhead < 5" | bc -l) )); then
+    if [[ "$overhead" != "N/A" ]] && awk "BEGIN {exit !($overhead < 5)}"; then
         {
             printf "%s✓ EXCELLENT: Overhead < 5%%%s\n" "$GREEN" "$NC"
             echo "  Front controller has negligible performance impact"
         } | tee -a "$REPORT_FILE"
-    elif (( $(echo "$overhead < 10" | bc -l) )); then
+    elif awk "BEGIN {exit !($overhead < 10)}"; then
         {
             printf "%s✓ GOOD: Overhead < 10%%%s\n" "$GREEN" "$NC"
             echo "  Front controller has acceptable performance impact"
         } | tee -a "$REPORT_FILE"
-    elif (( $(echo "$overhead < 20" | bc -l) )); then
+    elif awk "BEGIN {exit !($overhead < 20)}"; then
         {
             printf "%s⚠ ACCEPTABLE: Overhead < 20%%%s\n" "$YELLOW" "$NC"
             echo "  Front controller adds some overhead, consider optimization"
         } | tee -a "$REPORT_FILE"
     else
         {
             printf "%s✗ HIGH OVERHEAD: > 20%%%s\n" "$RED" "$NC"
             echo "  Front controller adds significant overhead, optimization needed"
         } | tee -a "$REPORT_FILE"
     fi
 fi
 {
     echo "Throughput Comparison:"
     echo "  Baseline: ${baseline_rps} req/sec"
     echo "  Front controller: ${fc_rps} req/sec"
     echo "  Login page: ${login_rps} req/sec"
-    if [[ -n "$static_rps" ]]; then
-        echo "  Static assets: ${static_rps} req/sec"
-    fi
+    [[ -n "${static_rps:-}" ]] && echo "  Static assets: ${static_rps} req/sec"
     echo "  REST API: ${api_rps} req/sec"
     echo
 } | tee -a "$REPORT_FILE"
-# Resource Usage (if available)
-if command -v ps &> /dev/null; then
-    php_procs=$(ps aux | grep php | grep -v grep | wc -l)
-    {
-        echo "Current Resource Usage:"
-        echo "  Active PHP processes: $php_procs"
-    } | tee -a "$REPORT_FILE"
-
-    if command -v top &> /dev/null; then
-        cpu_usage=$(grep "CPU usage" <<< "$(top -l 1)" | awk '{print $3}' || echo "N/A")
-        echo "  CPU usage: $cpu_usage" | tee -a "$REPORT_FILE"
-    fi
-    echo | tee -a "$REPORT_FILE"
-fi
+# Resource Usage (if available, cross-platform)
+if command -v pgrep &> /dev/null; then
+    php_procs=$(pgrep -c php || echo "0")
+    {
+        echo "Current Resource Usage:"
+        echo "  Active PHP processes: $php_procs"
+    } | tee -a "$REPORT_FILE"
+    
+    # CPU usage reporting is system-specific, so just note the count
+    echo "  (CPU usage monitoring requires system-specific tools)"
+    echo | tee -a "$REPORT_FILE"
+fi
 if [[ -n "$overhead" ]]; then
-    if (( $(echo "$overhead < 10" | bc -l) )); then
+    if [[ "$overhead" != "N/A" ]] && awk "BEGIN {exit !($overhead < 10)}"; then
         {
             echo "✓ Performance is acceptable for production use"
             echo "✓ Front controller adds minimal overhead"
             echo "✓ Security benefits outweigh performance impact"
         } | tee -a "$REPORT_FILE"
     else
🤖 Prompt for AI Agents
In tests/scripts/test_performance.sh around lines 184-296, the reporting block
lacks input validation, portable tools, and robust parsing; ensure baseline_time
and fc_time are validated as numbers and baseline_time > 0 before computing
overhead (skip or set overhead="N/A" on invalid/zero), replace bc-dependent
numeric comparisons with awk (which is more likely present) or bash-safe integer
checks so colorized verdicts work without bc, detect and parse CPU usage
portably (on Linux read /proc/stat or use top -b -n1; on macOS keep top -l 1)
with branching based on uname, quote all variable expansions like
"${static_rps}" to avoid empty-word issues, and change recommendation logic to
consider both overhead percentage and absolute delta_ms (compute
delta_ms=fc_time-baseline_time and factor a threshold so recommendations use
both percentage and absolute latency before signaling optimizations).

Comment on lines +29 to +37
public static function setUpBeforeClass(): void
{
self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
self::$client = new Client([
'base_uri' => self::$baseUrl,
'http_errors' => false,
'allow_redirects' => 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

Detect unreachable base URL before running compatibility checks

Like the security suite, this setup raises RequestException when no OpenEMR instance listens at OPENEMR_TEST_URL, causing the whole PHPUnit run to fail. Please wrap an initial request in a try/catch with a short timeout and skip (or fail fast with a helpful message) when the service is unavailable.

     public static function setUpBeforeClass(): void
     {
         self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
-        self::$client = new Client([
-            'base_uri' => self::$baseUrl,
-            'http_errors' => false,
-            'allow_redirects' => true,
-        ]);
+        self::$client = new Client([
+            'base_uri' => self::$baseUrl,
+            'http_errors' => false,
+            'allow_redirects' => true,
+            'timeout' => 5,
+        ]);
+
+        try {
+            self::$client->get('/index.php', ['allow_redirects' => false]);
+        } catch (RequestException $e) {
+            self::markTestSkipped(
+                'Front controller compatibility tests require a reachable OpenEMR instance at '
+                . self::$baseUrl . ' (' . $e->getMessage() . ')'
+            );
+        }
     }
📝 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
public static function setUpBeforeClass(): void
{
self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
self::$client = new Client([
'base_uri' => self::$baseUrl,
'http_errors' => false,
'allow_redirects' => true,
]);
}
public static function setUpBeforeClass(): void
{
self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
self::$client = new Client([
'base_uri' => self::$baseUrl,
'http_errors' => false,
'allow_redirects' => true,
'timeout' => 5,
]);
try {
// Quick probe to ensure the OpenEMR instance is up
self::$client->get('/index.php', ['allow_redirects' => false]);
} catch (RequestException $e) {
self::markTestSkipped(
'Front controller compatibility tests require a reachable OpenEMR instance at '
. self::$baseUrl . ' (' . $e->getMessage() . ')'
);
}
}
🤖 Prompt for AI Agents
In tests/Tests/FrontController/CompatibilityTest.php around lines 29-37, the
test suite unconditionally creates an HTTP client which later causes a
RequestException when no OpenEMR instance is reachable; add an initial
short-timeout probe request to the base URL inside setUpBeforeClass wrapped in
try/catch, and on exception either throw a PHPUnit skip (or
SkippedTestSuiteError) with a clear message or call the appropriate skip helper
so the suite is skipped rather than failing the entire run; ensure the probe
uses a small timeout and preserves existing client configuration when
proceeding.

Comment on lines +30 to +38
public static function setUpBeforeClass(): void
{
self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
self::$client = new Client([
'base_uri' => self::$baseUrl,
'http_errors' => false,
'allow_redirects' => true,
]);
self::loadVulnerableIncFiles();
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

Skip tests when the target instance is unreachable

Running the PHPUnit suite without a web server at OPENEMR_TEST_URL throws RequestException before any assertions run, breaking CI. Please probe the base URL with a short timeout and call self::markTestSkipped() (or fail with a clear message) when the endpoint is unreachable.

     public static function setUpBeforeClass(): void
     {
         self::$baseUrl = getenv('OPENEMR_TEST_URL') ?: 'http://localhost/openemr';
-        self::$client = new Client([
-            'base_uri' => self::$baseUrl,
-            'http_errors' => false,
-            'allow_redirects' => true,
-        ]);
-        self::loadVulnerableIncFiles();
+        self::$client = new Client([
+            'base_uri' => self::$baseUrl,
+            'http_errors' => false,
+            'allow_redirects' => true,
+            'timeout' => 5,
+        ]);
+
+        try {
+            self::$client->head('/index.php', ['allow_redirects' => false]);
+        } catch (RequestException $e) {
+            self::markTestSkipped(
+                'Front controller security tests require a reachable OpenEMR instance at '
+                . self::$baseUrl . ' (' . $e->getMessage() . ')'
+            );
+        }
+
+        self::loadVulnerableIncFiles();
     }

Comment on lines +41 to +54
private static function loadVulnerableIncFiles(): void
{
$basePath = dirname(dirname(__DIR__));
$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

Scan the real application tree for .inc.php files

dirname(dirname(__DIR__)) resolves to the tests/ directory, so self::$vulnerable_inc_files stays empty and all .inc.php assertions silently do nothing. Please root the scan at the project base (and optionally skip when no files are found).

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

After pointing to the application root, also consider failing or skipping when the collection stays empty so the test cannot silently pass without coverage.

📝 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(dirname(__DIR__));
$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 loader
roots the scan at dirname(dirname(__DIR__)) (the tests/ dir) so no .inc.php
files in the application are found; change the basePath to the
project/application root (e.g. resolve from getcwd() or climb from __DIR__ to
the repo root) and ensure the resolved path exists; after scanning, if
self::$vulnerable_inc_files is empty either fail the test explicitly or mark it
as skipped so assertions cannot silently no-op.

coding-sunshine added a commit that referenced this pull request Oct 29, 2025
…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:
coding-sunshine added a commit that referenced this pull request Oct 30, 2025
…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:
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