Skip to content

helper-based atomic-write cleanup; replace @unlink/@chmod#52

Merged
mambax7 merged 14 commits into
XOOPS:masterfrom
mambax7:fix/atomic-write-cleanup-helpers
May 11, 2026
Merged

helper-based atomic-write cleanup; replace @unlink/@chmod#52
mambax7 merged 14 commits into
XOOPS:masterfrom
mambax7:fix/atomic-write-cleanup-helpers

Conversation

@mambax7
Copy link
Copy Markdown
Contributor

@mambax7 mambax7 commented May 7, 2026

Changes:
cp_functions.php:
+ xoops_remove_file_quietly($path, $context): new helper for best-effort file removal. Skips non-existent paths, suppresses the unlink warning via
a SCOPED error_reporting() toggle (no @ operator) wrapped in try/finally, and re-checks file_exists() after a failed unlink — only logging when the
file is still present so TOCTOU races resolve silently. Uses xoops_file_label() for non-sensitive path labels in warnings.
- 9 @Unlink cleanup sites replaced with the helper.
- @chmod($tempFile, $perms) replaced with checked chmod() that logs on failure but continues (content is already written).

modules/system/class/maintenance.php:
+ Explicit require_once .../include/cp_functions.php so SystemMaintenance is self-sufficient regardless of which caller (admin, install,
modulesadmin, preferences) loads it.
- 6 atomic-write @Unlink cleanup sites replaced with the helper.
- @chmod replaced with checked + warn (mirrors cp_functions.php).
- cleanOrphanedAvatars(): @Unlink replaced with the helper plus a path-traversal-safe resolution. Avatar rows store 'avatars/'
(kernel/avatar.php and 14 admin/profile writers), so basename() would have stripped the directory and silently bypassed all orphan cleanup. The new form
normalises backslashes, strips leading slashes, resolves via realpath() with upload-root containment, and confirms is_file() before removal. The DB row
cleanup runs unconditionally.

Intentionally retained:
- The 7 @rename(...) calls inside if (!...) checks. These are the core atomic-move operations; the boolean return is already detected and reported
via trigger_error(). Removing the @ would let PHP's native warning fire alongside our diagnostic, double-reporting one event into display_errors
output. Comment block in each file documents the rationale.

Summary by CodeRabbit

  • New Features

    • Added filesystem safety helpers for quieter removal/chmod and safer path handling.
  • Bug Fixes

    • More reliable atomic file writes with quieter error handling, non-fatal permission attempts, and dependable temporary/backup cleanup and restore.
    • Hardened avatar cleanup: path normalization, strict resolved-path validation, and best-effort quiet deletion while still cleaning related database links.
    • Maintenance now reliably loads required file-safety routines.
  • Tests

    • Added unit tests covering avatar cleanup, path traversal, missing files, null-byte and Windows-style paths.

Review Change Stack

Changes:

    cp_functions.php:
      + xoops_remove_file_quietly($path, $context): new helper for
        best-effort file removal. Skips non-existent paths, suppresses
        the unlink warning via a SCOPED error_reporting() toggle (no @
        operator) wrapped in try/finally, and re-checks file_exists()
        after a failed unlink — only logging when the file is still
        present so TOCTOU races resolve silently. Uses
        xoops_file_label() for non-sensitive path labels in warnings.
      - 9 @Unlink cleanup sites replaced with the helper.
      - @chmod($tempFile, $perms) replaced with checked chmod() that
        logs on failure but continues (content is already written).

    modules/system/class/maintenance.php:
      + Explicit `require_once .../include/cp_functions.php` so
        SystemMaintenance is self-sufficient regardless of which
        caller (admin, install, modulesadmin, preferences) loads it.
      - 6 atomic-write @Unlink cleanup sites replaced with the helper.
      - @chmod replaced with checked + warn (mirrors cp_functions.php).
      - cleanOrphanedAvatars(): @Unlink replaced with the helper plus
        a path-traversal-safe resolution. Avatar rows store
        'avatars/<filename>' (kernel/avatar.php and 14 admin/profile
        writers), so basename() would have stripped the directory and
        silently bypassed all orphan cleanup. The new form:
          - normalises backslashes to '/' (Windows-historic data)
          - strips leading slashes (defends against absolute paths)
          - resolves via realpath() so '../' segments collapse
          - confirms containment under realpath(XOOPS_UPLOAD_PATH)
            with a trailing-separator boundary check
          - confirms is_file() before removal.
        The DB row cleanup runs unconditionally (file gone or not).

  Intentionally retained:
    - The 7 @rename(...) calls inside `if (!...)` checks. These are
      the core atomic-move operations; the boolean return is already
      detected and reported via trigger_error(). Removing the @ would
      let PHP's native warning fire alongside our diagnostic, double-
      reporting one event into display_errors output. Comment block
      in each file documents the rationale.
Copilot AI review requested due to automatic review settings May 7, 2026 19:53
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@mambax7 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6ebcd7c-4631-49a4-b937-e633ae1a8147

📥 Commits

Reviewing files that changed from the base of the PR and between f68b5bc and ca4130e.

📒 Files selected for processing (1)
  • htdocs/include/file_safety.php

Walkthrough

Adds scoped quiet chmod/unlink helpers, applies them to atomic-write and guard-file flows, hardens avatar deletion with path normalization and containment checks, requires the helpers in SystemMaintenance, and adds unit tests covering CleanAvatar() variants.

Changes

File Handling Safety and Error Recovery

Layer / File(s) Summary
Require & Remove
htdocs/include/cp_functions.php
Require include/file_safety.php early and remove local xoops_file_label() so helpers come from file_safety.php.
Atomic File Writing Updates
htdocs/include/cp_functions.php
xoops_write_file_atomically() replaces @unlink() with xoops_remove_file_quietly(), uses xoops_chmod_quietly() so chmod failures are non-fatal, and validates rename/backup/restore branches with quiet cleanup of temp/backup artifacts.
Maintenance Class Bootstrap
htdocs/modules/system/class/maintenance.php
Add direct-access guard and require include/file_safety.php to ensure quiet helpers are available.
Avatar Path Validation and Cleanup
htdocs/modules/system/class/maintenance.php
CleanAvatar() normalizes stored path (backslashes → slashes, strip leading /), rejects null bytes, resolves parent via realpath(), enforces containment within XOOPS_UPLOAD_PATH/avatars/, limits deletions to regular files or symlinks, removes via xoops_remove_file_quietly(), and returns DB-delete success status.
Guard File Write and Backup Recovery
htdocs/modules/system/class/maintenance.php
writeFileWithWarning() uses quiet helpers for guard/temp cleanup, tolerates chmod failures, validates backup creation and rename operations, attempts restore on failure, and cleans artifacts quietly.
Unit Tests
tests/unit/htdocs/modules/system/SystemMaintenanceTest.php
Adds helpers and multiple tests for CleanAvatar() covering valid deletion, traversal/absolute-path protection, missing/empty/null-byte handling, subdir confinement, Windows backslash normalization, and return-value contract when DB delete fails.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacement of error-suppressed filesystem calls (@unlink/@chmod) with helper-based atomic-write cleanup functions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

❌ Patch coverage is 20.45455% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.07%. Comparing base (47b528b) to head (ca4130e).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
htdocs/include/file_safety.php 0.00% 39 Missing ⚠️
htdocs/modules/system/class/maintenance.php 47.36% 20 Missing ⚠️
htdocs/include/cp_functions.php 0.00% 11 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master      #52      +/-   ##
============================================
- Coverage     18.08%   18.07%   -0.02%     
- Complexity     7840     7854      +14     
============================================
  Files           665      666       +1     
  Lines         42982    43142     +160     
============================================
+ Hits           7775     7798      +23     
- Misses        35207    35344     +137     

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

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces error-suppression usage in file cleanup paths by introducing a centralized “best-effort” delete helper and adopting it in atomic-write cleanup code, while also hardening orphaned-avatar deletion against path traversal.

Changes:

  • Added xoops_remove_file_quietly() in cp_functions.php and replaced multiple @unlink cleanup sites with the helper.
  • Replaced @chmod calls with checked chmod() that logs warnings on failure but continues.
  • Hardened SystemMaintenance::CleanAvatar() orphan cleanup by normalizing and resolving avatar paths and enforcing containment under XOOPS_UPLOAD_PATH.

Reviewed changes

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

File Description
htdocs/modules/system/class/maintenance.php Requires cp_functions.php, adopts the new cleanup helper in atomic-write cleanup, and hardens orphaned avatar file removal with realpath() + containment checks.
htdocs/include/cp_functions.php Introduces xoops_remove_file_quietly() and replaces multiple @unlink/@chmod usages in xoops_write_file_atomically().

Comment thread htdocs/modules/system/class/maintenance.php Outdated
Comment thread htdocs/modules/system/class/maintenance.php Outdated
…out of loop

  Address PR XOOPS#52 follow-up review (Copilot non-blocking comments):

  modules/system/class/maintenance.php (CleanAvatar):
    realpath(XOOPS_UPLOAD_PATH) and the trailing-separator prefix are
    constant for the whole run, so computing them per row in the
    while-loop is wasted filesystem work on installations with large
    orphaned-avatar tables. Hoisted outside the loop into \$uploadRoot
    and \$uploadRootPrefix; the in-loop check now uses str_starts_with
    against the cached prefix.

  tests/unit/htdocs/modules/system/SystemMaintenanceTest.php:
    Added six #[Test] cases covering the path-traversal-safe orphan
    cleanup logic introduced when @Unlink() was replaced by
    xoops_remove_file_quietly():

      - cleanAvatarRemovesValidAvatarFileUnderUploadRoot — happy path
      - cleanAvatarSkipsTraversalPathButStillDeletesDbRow — ../ blocked
      - cleanAvatarSkipsAbsolutePathButStillDeletesDbRow — /etc/hosts
      - cleanAvatarHandlesMissingFileAndStillDeletesDbRow
      - cleanAvatarHandlesEmptyAvatarFileAndStillDeletesDbRow
      - cleanAvatarNormalisesBackslashesInAvatarFile

    Each test asserts exec() is called exactly twice (avatar DELETE +
    avatar_user_link cleanup) regardless of file removal outcome, so
    the 'DB rows deleted regardless of file outcome' invariant is
    codified rather than relying on inspection.

    Filesystem fixtures live in a unique scratch subdirectory under
    XOOPS_UPLOAD_PATH/avatars/_test_<pid>_<uniqid> with finally-block
    cleanup so the tests do not collide with each other or with
    anything else in the upload tree.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +343 to +354
/**
* Stub the database mock so $db->query() returns a sentinel result and
* $db->fetchArray() yields the supplied avatar rows once, then false.
* Returns the mock for further expectations.
*/
private function stubAvatarSweep($db, array $rows): void
{
$db->method('query')->willReturn('mock_result');
$db->method('isResultSet')->willReturn(true);
$rows[] = false; // end-of-result sentinel
$db->method('fetchArray')->willReturnOnConsecutiveCalls(...$rows);
}
  CI run XOOPS#52 failed with 6 IncompatibleReturnValueException errors:
    Method exec may not return value of type int, its declared return type is \"bool\"

  XoopsMySQLDatabase::exec(string \$sql): bool returns true on success
  and false on failure (or false from PHP's mysqli_query for many DDL
  statements). The CleanAvatar() tests stubbed exec() with willReturn(1),
  which works under loose return-type checking but trips PHPUnit 11's
  strict return-type validation.

  Fix: all 6 exec() stubs use willReturn(true).
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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/modules/system/class/maintenance.php`:
- Line 200: The DELETE SQL concatenates $myrow['avatar_id'] directly into the
string; change the interpolation to cast the value to int before concatenation
to avoid injection/scan warnings—update the calls that use $this->db->exec(...)
with $this->db->prefix('avatar') and $myrow['avatar_id'] (the deletes at the
current line and the adjacent one around line 213) so the avatar_id is cast to
(int) (e.g., use (int)$myrow['avatar_id']) when building the WHERE clause.

In `@tests/unit/htdocs/modules/system/SystemMaintenanceTest.php`:
- Line 368: The mocks for XoopsDatabase::exec() use willReturn(1) which violates
exec()'s bool return type and causes PHPUnit IncompatibleReturnValueException;
locate every occurrence of $db->expects(...)->method('exec')->willReturn(1) in
the SystemMaintenanceTest cleanAvatar* tests and change willReturn(1) to
willReturn(true) (apply to all six instances where exec() is stubbed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3dc5b9f2-faa6-4f9e-b34c-1076d70a5758

📥 Commits

Reviewing files that changed from the base of the PR and between 1e64962 and b5fc7bf.

📒 Files selected for processing (2)
  • htdocs/modules/system/class/maintenance.php
  • tests/unit/htdocs/modules/system/SystemMaintenanceTest.php

Comment thread htdocs/modules/system/class/maintenance.php Outdated
Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +343 to +354
/**
* Stub the database mock so $db->query() returns a sentinel result and
* $db->fetchArray() yields the supplied avatar rows once, then false.
* Returns the mock for further expectations.
*/
private function stubAvatarSweep($db, array $rows): void
{
$db->method('query')->willReturn('mock_result');
$db->method('isResultSet')->willReturn(true);
$rows[] = false; // end-of-result sentinel
$db->method('fetchArray')->willReturnOnConsecutiveCalls(...$rows);
}
Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php Outdated
Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php Outdated
…st; (int) avatar_id

  Address PR XOOPS#52 follow-up review (Copilot, CodeRabbit Trivial):

  modules/system/class/maintenance.php (CleanAvatar):
    Cast $myrow['avatar_id'] to int once at the top of the loop body
    and use $avatarId in both DELETE statements (avatar row + the
    empty-avatar_file fast path). Defence-in-depth on the SQL concat
    even though the value is DB-origin; also silences SonarCloud's
    string-concatenation warning on these statements per project SQL
    hygiene.

  tests/unit/htdocs/modules/system/SystemMaintenanceTest.php:

    cleanAvatarSkipsTraversalPathButStillDeletesDbRow:
      The previous form created the fixture in sys_get_temp_dir() but
      used '../../<basename>' as the avatar_file. That string resolves
      from htdocs/uploads/ to a path under/near the project root —
      NOT the temp dir. realpath() returned false, the cleanup was
      skipped, and the test passed for the wrong reason: it never
      exercised the containment-prefix branch.

      New form: place the fixture at dirname(realpath(XOOPS_UPLOAD_PATH))
      so '../<basename>' actually resolves to it. realpath() now
      succeeds, and the prefix check is the rejection mechanism.
      Added an explicit assertSame() sanity assertion comparing the
      resolved path to realpath($outside) so the test self-documents
      that it really exercises the containment branch.

    cleanAvatarSkipsAbsolutePathButStillDeletesDbRow:
      Replaced the hard-coded /etc/hosts assertion with a temp fixture
      via tempnam(). Windows CI runners don't have /etc/hosts at the
      same path, and even on POSIX systems the test relied on a
      pre-existing system file. tempnam() works on every supported OS.

    stubAvatarSweep():
      Removed the misleading "Returns the mock for further expectations"
      docblock line. The method is `: void`; callers attach further
      expectations directly to the $db they passed in.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread htdocs/include/cp_functions.php Outdated
Comment thread htdocs/modules/system/class/maintenance.php Outdated
Comment on lines +203 to +217
$avatarId = (int) ($myrow['avatar_id'] ?? 0);
$avatarFile = ltrim(str_replace('\\', '/', (string) ($myrow['avatar_file'] ?? '')), '/');
if ('' === $avatarFile) {
$result1 = $this->db->exec('DELETE FROM ' . $this->db->prefix('avatar') . ' WHERE avatar_id=' . $avatarId);
continue;
}
$avatarPath = realpath(XOOPS_UPLOAD_PATH . '/' . $avatarFile);
if (
is_string($avatarPath)
&& is_string($uploadRootPrefix)
&& str_starts_with($avatarPath, $uploadRootPrefix)
&& is_file($avatarPath)
) {
xoops_remove_file_quietly($avatarPath, 'orphaned avatar');
}
  Address PR XOOPS#52 follow-up review (Copilot, 3 actionable):

  cp_functions.php:
    + xoops_chmod_quietly($path, $perms, $context): new helper
      mirroring xoops_remove_file_quietly(). Suppresses chmod()'s
      native PHP warning via the same scoped error_reporting() toggle
      + try/finally and emits a single project-standard trigger_error
      on the boolean false return. Without this, a single chmod
      failure produced TWO log lines (PHP warning + trigger_error).
    - The chmod() + trigger_error block in xoops_write_file_atomically()
      replaced with the helper.

  modules/system/class/maintenance.php:
    - chmod() + trigger_error block in writeFileWithWarning() replaced
      with the same helper (already require_once'ing cp_functions.php).
    - CleanAvatar(): containment check narrowed from
      realpath(XOOPS_UPLOAD_PATH) to realpath(XOOPS_UPLOAD_PATH .
      '/avatars'). Custom avatars are stored as 'avatars/<filename>'
      (kernel/avatar.php and 14 admin/edituser writers), so the broad
      upload-root check would have allowed deletion of any file under
      uploads/ if avatar_file pointed elsewhere — legacy data, custom-
      module write, or accidental insertion. The narrow prefix is
      defence-in-depth: skip rather than silently delete unrelated
      uploads. Hoisted out of the loop so the realpath() resolution
      is computed once per sweep.

  tests/unit/htdocs/modules/system/SystemMaintenanceTest.php:
    + cleanAvatarSkipsNonAvatarsSubdirUnderUploadRoot: places a
      fixture at uploads/files/<unique>.doc and sets avatar_file to
      'files/<unique>.doc'. Asserts the resolved path is under
      uploads/ (proving the test exercises the narrow-prefix branch,
      not the realpath-fail branch), runs CleanAvatar(), and verifies
      the fixture survives — codifying the narrowing as a regression
      test.

  No @ operators remain in either file's unlink/chmod paths; the
  seven @rename(...) calls inside `if (!...)` checks are still
  retained with the documented rationale.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php Outdated
…atar

  Address PR XOOPS#52 follow-up review (Copilot, line 316):

  placeFixtureAvatar() and the three inline file_put_contents() sites
  in cleanAvatar* tests previously ignored the return value. If the
  write failed (FS permissions, full disk, etc.) the happy-path tests
  could pass for the wrong reason — CleanAvatar() would find no file
  to delete and assertFileDoesNotExist() would succeed even though
  the cleanup never ran on a real fixture.

  All four fixture-creation sites now:
    - capture file_put_contents() return into $bytesWritten
    - assertNotFalse with a path-suffixed message for triage
    - assertSame(strlen(content), $bytesWritten) so partial writes
      are caught too, not just false-return failures

  Sites covered:
    - placeFixtureAvatar() (the one Copilot flagged)
    - cleanAvatarSkipsTraversalPathButStillDeletesDbRow
    - cleanAvatarSkipsAbsolutePathButStillDeletesDbRow
    - cleanAvatarSkipsNonAvatarsSubdirUnderUploadRoot
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
htdocs/modules/system/class/maintenance.php (1)

156-171: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add @throws \RuntimeException to the CleanAvatar() docblock.

The method throws a \RuntimeException (line 169-173) but the PHPDoc is silent about it. As per coding guidelines, public methods must declare @throws when applicable.

📝 Proposed docblock update
 /**
  * CleanAvatar
  *
  * Clean up orphaned custom avatars left when a user is deleted.
  *
  * `@author` slider84 of Team FrXoops
  *
+ * `@throws` \RuntimeException If the avatar table query fails.
  * `@return` boolean
  */

As per coding guidelines: "New public methods have PHPDoc with @param, @return, and @throws tags".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/modules/system/class/maintenance.php` around lines 156 - 171, The
CleanAvatar() method can throw a \RuntimeException but its PHPDoc lacks a
`@throws` tag; update the method's docblock for CleanAvatar to include "@throws
\RuntimeException" (with a brief description) so the public method documents the
possible exception thrown by the database error branch (the RuntimeException
raised around the $this->db->error() call).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/include/cp_functions.php`:
- Around line 138-187: Both xoops_chmod_quietly and xoops_remove_file_quietly
risk using an uninitialized $ok if a user error handler throws inside the try;
to fix, initialize $ok = false immediately before each try block (in
xoops_chmod_quietly and xoops_remove_file_quietly) so the subsequent if (!$ok)
checks are safe even when unlink()/chmod() never return due to an exception;
keep the scoped error_reporting() and existing trigger_error() behavior
unchanged.

In `@htdocs/modules/system/class/maintenance.php`:
- Around line 210-227: The two assignments to $result1 in the CleanAvatar method
are unused; remove the variable capture and call $this->db->exec(...) directly
(or alternatively check its boolean result and log/return false on failure).
Locate the two calls to $this->db->exec(...) that currently assign to $result1
in class Maintenance::CleanAvatar and either (A) replace " $result1 =
$this->db->exec(...);" with "$this->db->exec(...);" to make the intent explicit,
or (B) change to "if (!$this->db->exec(...)) { /* log via
processLogger/db->error and return false; */ }" so failures are propagated.

---

Outside diff comments:
In `@htdocs/modules/system/class/maintenance.php`:
- Around line 156-171: The CleanAvatar() method can throw a \RuntimeException
but its PHPDoc lacks a `@throws` tag; update the method's docblock for CleanAvatar
to include "@throws \RuntimeException" (with a brief description) so the public
method documents the possible exception thrown by the database error branch (the
RuntimeException raised around the $this->db->error() call).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 663a51de-03a6-4402-a4f1-f40999eb6701

📥 Commits

Reviewing files that changed from the base of the PR and between b5fc7bf and 1f4622c.

📒 Files selected for processing (3)
  • htdocs/include/cp_functions.php
  • htdocs/modules/system/class/maintenance.php
  • tests/unit/htdocs/modules/system/SystemMaintenanceTest.php

Comment thread htdocs/include/cp_functions.php Outdated
Comment thread htdocs/modules/system/class/maintenance.php
  Address PR XOOPS#52 follow-up review (CodeRabbit, 3 findings):

  cp_functions.php (xoops_chmod_quietly, xoops_remove_file_quietly):
    Initialise $ok = false before each try block. error_reporting(0)
    does NOT disable user-defined error handlers; only the native
    warning. A handler that throws (e.g. a strict ErrorException
    conversion that doesn't check error_reporting() & $errno) would
    propagate out of the try block before chmod()/unlink() returns,
    leaving $ok unset and triggering an "Undefined variable" warning
    on the subsequent if (!$ok) check. The defensive default makes
    the helpers safe under any error-handler shape.

  modules/system/class/maintenance.php (CleanAvatar):
    Drop the unused `$result1 = ` capture on both $this->db->exec()
    calls. PHPMD flagged this; the value was never read so the
    assignment was misleading (no error propagation either way).
    Removing makes the discard explicit.

    Also added `@throws \RuntimeException` to the CleanAvatar() docblock
    per project convention — the method does throw RuntimeException
    when the avatar query fails, but the PHPDoc was silent about it.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
htdocs/modules/system/class/maintenance.php (1)

278-358: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

writeFileWithWarning() atomic-write paths are consistent with the new helper conventions.

Helper routing for temp-guard and backup-guard cleanup is uniform, the chmod demotion to non-fatal matches xoops_write_file_atomically() semantics, and the $backupFile = null initialisation at Line 325 cleanly disambiguates the "no prior file" path from the "restoration needed" path at Lines 347–351.

One minor observability nit (not blocking): when the initial replace fails but @rename($backupFile, $filename) succeeds, the user sees only Failed to replace guard file, with no indication that the original was restored. Tracking it as something to consider if the message ever surfaces in support tickets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/modules/system/class/maintenance.php` around lines 278 - 358, The
failure path in writeFileWithWarning() currently logs only "Failed to replace
guard file" when rename(temp->target) fails even if the subsequent restore of
the backup ($backupFile -> $filename) succeeds; update writeFileWithWarning() so
that inside the if (!@rename($tempFile, $filename)) { ... if ($backupFile !==
null) { if (!@rename($backupFile, $filename)) { ... } } } block you detect the
return of `@rename`($backupFile, $filename) and emit an additional, clear log when
the restore succeeds (e.g. a trigger_error/notice like "Restored original guard
file: %s" using $label) so callers can observe that the original was restored;
keep the existing error path when the restore fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@htdocs/modules/system/class/maintenance.php`:
- Around line 278-358: The failure path in writeFileWithWarning() currently logs
only "Failed to replace guard file" when rename(temp->target) fails even if the
subsequent restore of the backup ($backupFile -> $filename) succeeds; update
writeFileWithWarning() so that inside the if (!@rename($tempFile, $filename)) {
... if ($backupFile !== null) { if (!@rename($backupFile, $filename)) { ... } }
} block you detect the return of `@rename`($backupFile, $filename) and emit an
additional, clear log when the restore succeeds (e.g. a trigger_error/notice
like "Restored original guard file: %s" using $label) so callers can observe
that the original was restored; keep the existing error path when the restore
fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d53766d-6c3d-48ac-8b53-1513a1b1f78a

📥 Commits

Reviewing files that changed from the base of the PR and between 87e6cc5 and efc6796.

📒 Files selected for processing (2)
  • htdocs/include/cp_functions.php
  • htdocs/modules/system/class/maintenance.php

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (1)

htdocs/modules/system/class/maintenance.php:248

  • CleanAvatar() always returns true and ignores the return values from the DELETE exec() calls (including the final avatar_user_link cleanup, currently assigned to $result2 but never checked). Since callers use the boolean return to decide whether the avatar purge succeeded (e.g. system/admin/maintenance/main.php), failures here will be reported as success. Consider tracking exec() results and returning false when a delete fails (or removing the boolean return entirely if you don't want callers to rely on it).
            $this->db->exec('DELETE FROM ' . $this->db->prefix('avatar') . ' WHERE avatar_id=' . $avatarId);
        }
        //clean any deleted users from avatar_user_link table
        $result2 = $this->db->exec('DELETE FROM ' . $this->db->prefix('avatar_user_link') . ' WHERE user_id NOT IN (SELECT uid FROM ' . $this->db->prefix('users') . ')');

        return true;

… writeFileWithWarning

  modules/system/class/maintenance.php:

    CleanAvatar() — return contract:
      The method's @return bool was always returning true regardless
      of whether any DELETE actually succeeded. If a row delete or
      the final avatar_user_link cleanup failed, the orphaned row
      was left behind and the caller had no way to know.

      Track a $deleteOk flag across all three exec() sites
      (empty-avatar_file branch, regular avatar-row branch, final
      avatar_user_link cleanup) and return it. Existing CleanAvatar
      tests stub exec() with willReturn(true) so $deleteOk stays
      true and the return value is unchanged in the happy path.

    writeFileWithWarning() — restore observability:
      When the rename($tempFile, $filename) replace step fails but
      the rename($backupFile, $filename) restore step succeeds, the
      log previously said only "Failed to replace guard file: <X>",
      leaving operators unsure whether manual recovery was needed.

      Track $restoredBackup and fold the status into the existing
      composite failure warning:
        "Failed to replace guard file: <X> (original restored)"
      when the restore worked, or the unsuffixed form (plus the
      pre-existing "Failed to restore original guard file" line)
      when both steps failed and manual intervention is needed.
      No additional success warnings introduced.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread htdocs/include/cp_functions.php Outdated
Comment on lines +138 to +202
function xoops_chmod_quietly($path, $perms, $context = 'temp')
{
// Initialise $ok before the try block: error_reporting(0) does NOT
// disable user-defined error handlers, only the native warning. A
// naively written handler that always throws (without checking
// error_reporting() & $errno) would propagate out of the try block
// before chmod() returns, leaving $ok unset. Defensive default.
$ok = false;
$previousLevel = error_reporting(0);
try {
$ok = chmod($path, $perms);
} finally {
error_reporting($previousLevel);
}
if (!$ok) {
trigger_error(
sprintf('Failed to set permissions on %s file: %s', $context, xoops_file_label($path)),
E_USER_WARNING
);
}

return $ok;
}

/**
* Best-effort file removal used by atomic-write cleanup paths and similar
* fire-and-forget cleanup. Skips non-existent paths so already-deleted
* files don't trigger warnings, suppresses the unlink() warning via a
* scoped error_reporting() toggle (no `@` operator), and re-checks
* existence after a failed unlink — only logging when the file is still
* present, so TOCTOU races resolve silently.
*
* @param string $path Absolute path to the file to remove.
* @param string $context Short label used in the warning message
* (e.g. 'temporary', 'backup').
*
* @return void
*/
function xoops_remove_file_quietly($path, $context = 'temporary')
{
// file_exists() returns false for broken symlinks, so a dangling
// symlink would be skipped here and also bypass the post-unlink
// existence check below — leaving the orphaned link in place. Treat
// links as existing too: unlink() can remove broken symlinks just
// fine, and the targets they point to are not what we care about.
if (!file_exists($path) && !is_link($path)) {
return;
}
// Initialise $ok defensively — see xoops_chmod_quietly() for the
// rationale (error_reporting(0) does not disable user-defined
// error handlers).
$ok = false;
$previousLevel = error_reporting(0);
try {
$ok = unlink($path);
} finally {
error_reporting($previousLevel);
}
if (!$ok && (file_exists($path) || is_link($path))) {
trigger_error(
sprintf('Failed to remove %s file: %s', $context, xoops_file_label($path)),
E_USER_WARNING
);
}
}
Comment on lines +233 to +250
$avatarId = (int) ($myrow['avatar_id'] ?? 0);
$avatarFile = ltrim(str_replace('\\', '/', (string) ($myrow['avatar_file'] ?? '')), '/');
if ('' === $avatarFile) {
if (!$this->db->exec('DELETE FROM ' . $this->db->prefix('avatar') . ' WHERE avatar_id=' . $avatarId)) {
$deleteOk = false;
}
continue;
}
$avatarCandidate = XOOPS_UPLOAD_PATH . '/' . $avatarFile;
$avatarParent = realpath(dirname($avatarCandidate));
if (
is_string($avatarParent)
&& is_string($avatarRootPrefix)
&& str_starts_with(rtrim($avatarParent, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $avatarRootPrefix)
&& (is_file($avatarCandidate) || is_link($avatarCandidate))
) {
xoops_remove_file_quietly($avatarCandidate, 'orphaned avatar');
}
…ll-byte paths

  xoops_chmod_quietly() / xoops_remove_file_quietly(): wrap the fs syscall
  (and, in the remove helper, the file_exists()/is_link() probes) in
  catch(\Throwable) so a ValueError from a "\0"-bearing path — or a
  throwing user error handler — cannot abort the caller's unrelated work.

  CleanAvatar(): gate dirname/realpath/is_file/is_link/remove on
  "\0" not in normalized avatar_file; collapse the empty-string special
  branch into the same condition. Per-row and avatar_user_link DELETEs
  still run unconditionally so a malformed path never blocks reclaiming
  the orphaned row.

  Tests: add cleanAvatarHandlesNullByteAvatarFileAndStillDeletesDbRow,
  covering the ValueError-skipped path with both DELETEs asserted.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/include/cp_functions.php`:
- Around line 159-163: The trigger_error calls currently expose root-relative
paths by using xoops_file_label($path); change those warning messages to include
only the basename of the path (use basename($path) or an equivalent safe helper)
instead of xoops_file_label($path) in the trigger_error calls (the ones invoking
trigger_error(sprintf('Failed to set permissions on %s file: %s', $context,
xoops_file_label($path)), E_USER_WARNING) and the similar block at the other
occurrence), so non-fatal filesystem warnings log only the filename and not the
full install layout.

In `@tests/unit/htdocs/modules/system/SystemMaintenanceTest.php`:
- Around line 366-592: The tests exercise filesystem branches but never assert
the new boolean return from CleanAvatar(); update each existing test that calls
$maintenance->CleanAvatar() to assert the returned bool (e.g.
$this->assertTrue($maintenance->CleanAvatar(), ...)) and add one new unit test
that uses createMockDatabase()/stubAvatarSweep() and sets
$db->expects(...)->method('exec')->willReturnOnConsecutiveCalls(true, false) (or
otherwise simulate a DELETE failure) then
assertFalse($maintenance->CleanAvatar()) to cover the failing-exec path;
reference the CleanAvatar() call, createMockDatabase(), stubAvatarSweep(), and
the mock exec() expectations to locate where to add these assertions.
- Around line 366-592: Rename each PHPUnit test method to the test... convention
so PHPUnit 9.6 discovers them: change
cleanAvatarRemovesValidAvatarFileUnderUploadRoot() →
testCleanAvatarRemovesValidAvatarFileUnderUploadRoot(),
cleanAvatarSkipsTraversalPathButStillDeletesDbRow() →
testCleanAvatarSkipsTraversalPathButStillDeletesDbRow(),
cleanAvatarSkipsAbsolutePathButStillDeletesDbRow() →
testCleanAvatarSkipsAbsolutePathButStillDeletesDbRow(),
cleanAvatarHandlesMissingFileAndStillDeletesDbRow() →
testCleanAvatarHandlesMissingFileAndStillDeletesDbRow(),
cleanAvatarHandlesEmptyAvatarFileAndStillDeletesDbRow() →
testCleanAvatarHandlesEmptyAvatarFileAndStillDeletesDbRow(),
cleanAvatarHandlesNullByteAvatarFileAndStillDeletesDbRow() →
testCleanAvatarHandlesNullByteAvatarFileAndStillDeletesDbRow(),
cleanAvatarSkipsNonAvatarsSubdirUnderUploadRoot() →
testCleanAvatarSkipsNonAvatarsSubdirUnderUploadRoot(), and
cleanAvatarNormalisesBackslashesInAvatarFile() →
testCleanAvatarNormalisesBackslashesInAvatarFile(); keep the existing #[Test]
attributes or remove them if you prefer, but the critical change is renaming the
methods so PHPUnit 9.6 recognizes the tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc364852-5811-4398-a9fd-1bae3f576321

📥 Commits

Reviewing files that changed from the base of the PR and between 87e6cc5 and c0cb1de.

📒 Files selected for processing (3)
  • htdocs/include/cp_functions.php
  • htdocs/modules/system/class/maintenance.php
  • tests/unit/htdocs/modules/system/SystemMaintenanceTest.php

Comment thread htdocs/include/cp_functions.php Outdated
Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +19 to 31
// execute outside a bootstrapped XOOPS context. Without this, the
// require_once below would fail with an "undefined constant" fatal
// when the file is hit via a direct URL, leaking server path details
// in the error message. Uses the project-standard one-liner shape
// for consistency with the rest of the codebase.
defined('XOOPS_ROOT_PATH') || exit('Restricted access');

// xoops_remove_file_quietly() lives in cp_functions.php; admin and install
// callers normally load it via cp_header.php / page_moduleinstaller.php,
// but require it explicitly here so SystemMaintenance is self-sufficient
// regardless of which context instantiates it.
require_once XOOPS_ROOT_PATH . '/include/cp_functions.php';

Comment on lines +331 to +335
/**
* Recursively remove the per-test scratch directory.
*/
private function removeScratchDir(string $scratchRel): void
{
… CleanAvatar test coverage

  Extract xoops_file_label, xoops_safe_basename, xoops_chmod_quietly, and
  xoops_remove_file_quietly into a new side-effect-free include —
  htdocs/include/file_safety.php. cp_functions.php now requires that
  file so existing CP call sites work unchanged; SystemMaintenance
  requires only file_safety.php and no longer pulls in
  cp_functions.php's XOOPS_CPFUNC_LOADED define, which was silently
  forcing redirect_header() into the 'default' theme during the upgrade
  flows that instantiate SystemMaintenance.

  Narrow the cleanup-helper warnings to basename-only labels via a new
  xoops_safe_basename(); atomic-write callers keep xoops_file_label()
  for broader install-relative context.

  Tests: rename the eight new CleanAvatar test methods to the test...
  prefix for PHPUnit 9.6 discovery (keeping #[Test] for 11.x), assert
  the boolean contract on every happy-path call, and add
  testCleanAvatarReturnsFalseWhenAvatarDeleteFails() to lock in the
  false-return path. Fix the removeScratchDir() docblock to match the
  single-level implementation.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +26 to +27
require_once __DIR__ . '/file_safety.php';

Comment on lines +37 to 38
require_once XOOPS_ROOT_PATH . '/include/file_safety.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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/modules/system/class/maintenance.php`:
- Line 37: The SystemMaintenance class currently hard-requires the new helper at
the path used in the require_once statement (the line calling require_once
XOOPS_ROOT_PATH . '/include/file_safety.php'), but that helper file hasn't been
added to the repo so loading fatals; add and commit the missing helper file at
include/file_safety.php (implement the expected functions/constants used by
SystemMaintenance) so the require_once succeeds, or if you prefer a safer
interim change, replace the hard require with a guarded include_once using
file_exists(...) and fallback behavior so SystemMaintenance can load while the
helper is added.

In `@tests/unit/htdocs/modules/system/SystemMaintenanceTest.php`:
- Around line 336-352: The test helper removeScratchDir currently suppresses
errors with `@unlink/`@rmdir which hides cleanup failures; update removeScratchDir
to explicitly check for file/directory existence and assert successful deletions
instead of silencing errors: iterate entries in the directory returned by
scandir, skip '.'/'..', build $path = $scratchAbs . '/' . $entry, if
is_file($path) assert that unlink($path) returns true (or throw an
exception/assert with a clear message), and after the loop assert that
rmdir($scratchAbs) returns true (or throw/assert) so failures surface; keep
using uploadAbs to compute $scratchAbs and preserve the early return when
!is_dir($scratchAbs).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ccc14e1-a7be-4195-9ae8-4bb45e66c5d0

📥 Commits

Reviewing files that changed from the base of the PR and between 87e6cc5 and 4723bd5.

📒 Files selected for processing (3)
  • htdocs/include/cp_functions.php
  • htdocs/modules/system/class/maintenance.php
  • tests/unit/htdocs/modules/system/SystemMaintenanceTest.php

Comment thread htdocs/modules/system/class/maintenance.php
Comment thread tests/unit/htdocs/modules/system/SystemMaintenanceTest.php
  Commit 4723bd5 modified cp_functions.php and maintenance.php to
  require htdocs/include/file_safety.php but did not stage the new
  file itself. Every PHP CI job failed at PHPUnit bootstrap with
  \"Failed opening required '.../htdocs/include/file_safety.php'\".

  Adds the file. No code change — contents are exactly what was reviewed
  previously: xoops_file_label, xoops_safe_basename, xoops_chmod_quietly,
  and xoops_remove_file_quietly, each function_exists()-guarded with no
  module-level side effects beyond the XOOPS_ROOT_PATH direct-access exit.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +121 to +128
if (!$ok) {
// basename-only label: cleanup-helper warnings never need
// directory context, and the strict form keeps install
// layout out of any error log a site operator may share.
trigger_error(
sprintf('Failed to set permissions on %s file: %s', $context, xoops_safe_basename($path)),
E_USER_WARNING
);
  CodeRabbit observation: xoops_safe_basename() served the cleanup
  helpers' best-effort / non-propagating contract via raw basename().
  Empirically basename() does not throw on a "\0"-bearing path in PHP
  8.2-8.4 (it returns the byte verbatim), but the helpers it serves
  guarantee no exception escapes — that guarantee must hold under any
  future PHP, a userland override, or a throwing error handler. A
  literal "\0" in trigger_error() output also confuses log parsers.

  xoops_safe_basename() now rejects "\0"-bearing input up front and
  wraps basename() in catch(\Throwable), returning the fixed
  placeholder "invalid-path" on either branch. Same shape as
  xoops_chmod_quietly() / xoops_remove_file_quietly() — defence in
  depth without changing the warning-on-failure contract.

  Adds tests/unit/htdocs/include/FileSafetyTest.php with seven cases:
  - xoops_safe_basename: null-byte → placeholder, backslash
    normalisation, ordinary pass-through.
  - xoops_chmod_quietly: null-byte path returns false, fires one
    E_USER_WARNING containing "invalid-path", does not throw.
  - xoops_remove_file_quietly: null-byte path returns silently;
    missing path is a no-op with no warning.
  - xoops_file_label: strip-XOOPS_ROOT_PATH-prefix branch and
    basename fallback
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Comment on lines +205 to +212
// Same try/catch shape around the post-unlink probe: if the path
// contained a null byte we have nothing useful to report anyway.
try {
$stillPresent = file_exists($path) || is_link($path);
} catch (\Throwable $e) {
$stillPresent = false;
}
if (!$ok && $stillPresent) {
@mambax7 mambax7 merged commit c224880 into XOOPS:master May 11, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants