Skip to content

fix(system): hydrate module fields when cloning a block (#73)#77

Merged
mambax7 merged 6 commits into
XOOPS:masterfrom
mambax7:fix/issue-73-clone-module-block
May 17, 2026
Merged

fix(system): hydrate module fields when cloning a block (#73)#77
mambax7 merged 6 commits into
XOOPS:masterfrom
mambax7:fix/issue-73-clone-module-block

Conversation

@mambax7
Copy link
Copy Markdown
Contributor

@mambax7 mambax7 commented May 17, 2026

Closes #73.

Problem

Cloning a module block (block_type=D) submits bid=0, so case 'save' in modules/system/admin/blocksadmin/main.php builds a fresh empty block via create(). The handler never read the module metadata back from the hidden clone-form fields, so:

  1. getVar('name') was ''"name is required" DB validation error.
  2. mid / func_file / show_func / edit_func / template / dirname saved empty → clone renders nothing on the front end and shows no options panel in admin.

Edits (bid>0) were unaffected because get() loads everything from the DB.

Fix

In the bid==0 branch, hydrate the module fields from POST — exactly the set SystemBlock::getForm('clone') round-trips (mid, func_num, func_file, show_func, edit_func, template, dirname, name). Verified against block.php::getForm() (hidden fields, lines 188–196).

Scope / regression safety: change is fully contained in the clone branch. Normal edits still use get(). Custom-block clones are unaffected — isCustom() derives name from c_type and ignores these (posted empty).

Trust model unchanged: these fields are admin-only + CSRF-gated and already round-trip through the clone form; this restores intended behavior, introduces no new exposure. A stronger design (carry a source_bid, re-fetch + validate dirname/func server-side) is a larger, separate hardening — not required to fix #73; can file as a follow-up if wanted.

Test

tests/unit/htdocs/modules/system/BlocksAdminCloneTest.php — static source analysis, consistent with the codebase's existing ModulesAdminTest/SourceFileTestTrait pattern (this handler is procedural — redirects/CSRF/globals — and not executable in isolation). Covers: clone branch reads all 8 fields from POST; name is hydrated before insert() (failure mode #1); the edit path still uses get().

Honest coverage note: this is source-level verification, not an executed end-to-end handler test (same approach/limitation as ModulesAdminTest). It is a genuine regression guard — the setVars([...]) capture does not match the pre-fix code, so it fails without the fix. I validated every assertion deterministically against the patched source locally; I could not run the actual PHPUnit suite in this environment.

Test plan

  • CI green (PHPUnit 11 matrix runs BlocksAdminCloneTest)
  • Manual: clone a module block via System → Blocks Admin → submit unchanged → clone saves with module binding intact, no name error, options panel present

Summary by CodeRabbit

  • Bug Fixes

    • Block cloning now preserves module association, template/name metadata and passes name validation.
    • Added validation to reject tampered or unsafe path/function inputs during cloning; users see an "Invalid block parameters." notice on failure.
  • Tests

    • Added regression tests covering clone-field hydration, name validation ordering, and rejection of malformed path/function inputs.
  • Documentation

    • New admin language entry for the invalid-clone notice.

Review Change Stack

Cloning a module block posts bid=0, so case 'save' creates a fresh
empty block. The handler never read the module metadata back from the
hidden clone-form fields, so the clone failed the not-null name
validation and lost its module association (empty mid/func_*/dirname →
renders nothing, no options panel).

In the bid==0 branch, hydrate mid, func_num, func_file, show_func,
edit_func, template, dirname and name from POST (the exact set
SystemBlock::getForm('clone') round-trips). Edits (bid>0) are
unchanged — get() still loads from the DB; custom-block clones are
unaffected (isCustom() path ignores these and posts them empty).

Adds BlocksAdminCloneTest (static source analysis, like ModulesAdminTest
— the handler is procedural and not executable in isolation) covering
both failure modes and guarding the edit path.
Copilot AI review requested due to automatic review settings May 17, 2026 00:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@mambax7 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 24 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: 42ccb916-02a2-479e-85c9-31640e6e3b57

📥 Commits

Reviewing files that changed from the base of the PR and between 794f2e0 and 58e20ae.

📒 Files selected for processing (2)
  • htdocs/modules/system/admin/blocksadmin/main.php
  • tests/unit/htdocs/modules/system/BlocksAdminCloneTest.php

Walkthrough

This PR fixes module block cloning in the XOOPS Block Admin save flow: when bid=0 the handler reads hidden clone POST fields (mid, func_num, func_file, show_func, edit_func, template, dirname, name), validates path and function-name inputs, hydrates the new block object before insert, and adds regression tests plus language/changelog entries.

Changes

Module Block Clone Fix

Layer / File(s) Summary
Clone handler implementation with validation
htdocs/modules/system/admin/blocksadmin/main.php, htdocs/modules/system/language/english/admin/blocksadmin.php
When bid=0, reads clone hidden fields from POST, validates dirname, func_file, template (reject .., separators, backslashes, NUL, absolute paths) and show_func/edit_func (PHP identifier allowlist), redirects on invalid input, and hydrates the new block via setVars (mid, func_num, func_file, show_func, edit_func, template, dirname, name) before insert.
Regression tests and changelog
tests/unit/htdocs/modules/system/BlocksAdminCloneTest.php, docs/changelog.270.txt, docs/lang_diff.txt
Adds BlocksAdminCloneTest static assertions covering clone-branch POST hydration, tampering rejection (detection of .. + use of redirect_header), ordering that reads name before insert, and edit-path regression guard; documents the change in changelog and language diff entry.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(system): hydrate module fields when cloning a block (#73)' clearly and concisely describes the main change—fixing module field hydration during block cloning.
Linked Issues check ✅ Passed The pull request fully addresses issue #73 by hydrating all required module fields (mid, func_num, func_file, show_func, edit_func, template, dirname, name) when bid=0 and adding validation to prevent traversal attacks.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the clone-branch module hydration issue and its hardening: core logic fixes, validation, language string, unit tests, and changelog documentation.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

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

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

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 17, 2026

Codecov Report

❌ Patch coverage is 0% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.12%. Comparing base (62d52ff) to head (58e20ae).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
htdocs/modules/system/admin/blocksadmin/main.php 0.00% 31 Missing ⚠️
...ules/system/language/english/admin/blocksadmin.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master      #77      +/-   ##
============================================
- Coverage     18.13%   18.12%   -0.02%     
  Complexity     7854     7854              
============================================
  Files           666      666              
  Lines         43208    43240      +32     
============================================
  Hits           7837     7837              
- Misses        35371    35403      +32     

☔ 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

Fixes system Blocks Admin cloning for module blocks (block_type=D) by ensuring that when a clone is submitted with bid=0, the save handler re-hydrates the module-binding fields from the clone form so the cloned block retains its module association and passes name validation (issue #73).

Changes:

  • Hydrate module block metadata (mid/func/file/functions/template/dirname/name) from POST when bid=0 in the blocks save handler.
  • Add a static source-analysis regression test to ensure the clone branch hydrates the expected fields and the edit path remains unchanged.
  • Document the fix in the 2.7.0 changelog.

Reviewed changes

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

File Description
htdocs/modules/system/admin/blocksadmin/main.php Hydrates module-binding fields in the bid=0 (clone/create) save branch via $block->setVars([...]).
tests/unit/htdocs/modules/system/BlocksAdminCloneTest.php Adds a static regression test that asserts the clone branch reads all expected fields from POST and that edit path still uses get($block_id).
docs/changelog.270.txt Notes the module-block clone hydration fix for issue #73.

Comment on lines +296 to +305
$block->setVars([
'mid' => Request::getInt('mid', 0, 'POST'),
'func_num' => Request::getInt('func_num', 0, 'POST'),
'func_file' => Request::getString('func_file', '', 'POST'),
'show_func' => Request::getString('show_func', '', 'POST'),
'edit_func' => Request::getString('edit_func', '', 'POST'),
'template' => Request::getString('template', '', 'POST'),
'dirname' => Request::getString('dirname', '', 'POST'),
'name' => Request::getString('name', '', 'POST'),
]);
…review)

PR XOOPS#77 review: the clone branch persists dirname/func_file/template
(later concatenated into include_once paths) and show_func/edit_func
(called as functions) from hidden POST inputs. Legitimate clone values
are plain module identifiers copied from a trusted DB row, so add a
strict reject-on-tamper guard that never rejects real clones but blocks
path traversal / code injection via a forged POST:

- dirname/func_file/template: reject if they contain / \ .. or NUL
- show_func/edit_func: must match ^[A-Za-z_][A-Za-z0-9_]*$ (or empty)
- on violation: redirect_header(), do not persist

Not using Request::getCmd() (it lowercases/strips and would silently
corrupt valid mixed-case identifiers — the same broken-clone class this
issue fixes). Test extended to cover the guard; all assertions verified
against source. Scoped to the bid==0 clone path; edits (bid>0) load
from the trusted DB and are untouched.
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 +314 to +320
redirect_header('admin.php?fct=blocksadmin', 3, 'Invalid block parameters.');
}
}
foreach ([$clone_show_func, $clone_edit_func] as $clone_func) {
if ($clone_func !== '' && !preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $clone_func)) {
redirect_header('admin.php?fct=blocksadmin', 3, 'Invalid block parameters.');
}
}

/**
* Hardening (PR #77 review): the clone branch must reject tampered
Without the /u flag, PCRE \w is exactly [A-Za-z0-9_], so
/^[A-Za-z_][A-Za-z0-9_]*$/ -> /^[A-Za-z_]\w*$/ is identical (leading
class keeps no-digit-start). Coupled test assertion updated in lockstep;
regex behaviour re-verified (b_News_show matches, ../evil rejected).
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 +314 to +319
redirect_header('admin.php?fct=blocksadmin', 3, 'Invalid block parameters.');
}
}
foreach ([$clone_show_func, $clone_edit_func] as $clone_func) {
if ($clone_func !== '' && !preg_match('/^[A-Za-z_]\w*$/', $clone_func)) {
redirect_header('admin.php?fct=blocksadmin', 3, 'Invalid block parameters.');
}
foreach ([$clone_show_func, $clone_edit_func] as $clone_func) {
if ($clone_func !== '' && !preg_match('/^[A-Za-z_]\w*$/', $clone_func)) {
redirect_header('admin.php?fct=blocksadmin', 3, 'Invalid block parameters.');
PR XOOPS#77 review (both valid):
- replace the hard-coded 'Invalid block parameters.' redirect with a
  new _AM_SYSTEM_BLOCKS_INVALIDCLONE language constant, matching this
  handler's i18n convention; constant added to the english admin
  blocksadmin language file and recorded in docs/lang_diff.txt.
- reword the regression-test docblock to reference issue XOOPS#73 instead of
  a transient PR number (clearer in blame/history).

Stale: the original "sanitize these fields" comment (commit 368bc926)
is already addressed by the a1c260a hardening. CodeRabbit approved.
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 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines +302 to +316
// Reject tampered hidden inputs. dirname/func_file/template are
// later concatenated into include_once paths and show/edit_func
// are called as functions when the block renders. Legitimate
// clone values are plain module identifiers copied from a
// trusted DB row, so a strict check never rejects real clones
// but blocks path traversal / code injection via forged POST.
foreach ([$clone_dirname, $clone_func_file, $clone_template] as $clone_path) {
if ($clone_path !== ''
&& (false !== strpos($clone_path, '/')
|| false !== strpos($clone_path, '\\')
|| false !== strpos($clone_path, '..')
|| false !== strpos($clone_path, "\0"))) {
redirect_header('admin.php?fct=blocksadmin', 3, _AM_SYSTEM_BLOCKS_INVALIDCLONE);
}
}
Copilot review (verified valid): core builds module-block paths by
plain concatenation modules/<dirname>/blocks/<func_file> with no
enforcement that func_file is flat, so a module may legitimately nest a
block file (sub/foo.php). The previous guard rejected any '/' in
func_file/template, which would break cloning such blocks — a real BC
regression for the ~200-module ecosystem.

Relax: dirname stays single-segment-strict (no / \ .. NUL); func_file
and template now allow an internal '/' but still reject '..',
backslash, NUL and absolute paths. Equally secure against the actual
threat (a forged value cannot escape modules/<dirname>/blocks/, and a
system admin can already run PHP via custom blocks); no BC regression.
Behaviour verified: system_blocks.php and sub/foo.php accepted;
../../x, /abs, a\b rejected.
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 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +157 to +158
$insertPos,
$nameRead,
Comment on lines +302 to +305
// Reject tampered hidden inputs. dirname/func_file/template are
// later concatenated into include_once paths and show/edit_func
// are called as functions when the block renders. Legitimate
// clone values come from a trusted DB row, so these checks never
PR XOOPS#77 review:
- Copilot (valid): the guard comment claimed `template` is concatenated
  into an include_once path. Only dirname/func_file locate the block PHP
  file; `template` is persisted as the block's Smarty template name.
  Reworded both comments accordingly. Validation is unchanged
  (defense-in-depth still applies to a Smarty template name).
- Copilot (incorrect): claimed the assertLessThan args were reversed.
  PHPUnit assertLessThan($expected,$actual) passes when $actual<$expected,
  so assertLessThan($insertPos,$nameRead) already enforced
  nameRead<insertPos. Not inverting it (that would break the test);
  rewrote as the equivalent assertGreaterThan($nameRead,$insertPos) so
  it reads naturally and stops recurring review confusion.
@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 5 out of 5 changed files in this pull request and generated no new comments.

@mambax7 mambax7 merged commit 463aa31 into XOOPS:master May 17, 2026
10 of 12 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.

[Bug]: Cloning a module block loses all module fields and fails name validation

2 participants