Skip to content

Feat/shared dhtml toolbar - #151

Merged
mambax7 merged 8 commits into
XOOPS:masterfrom
mambax7:feat/shared-dhtml-toolbar
Aug 7, 2026
Merged

Feat/shared dhtml toolbar#151
mambax7 merged 8 commits into
XOOPS:masterfrom
mambax7:feat/shared-dhtml-toolbar

Conversation

@mambax7

@mambax7 mambax7 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Introduce a shared, framework-neutral DHTML editor toolbar for XoopsFormDhtmlTextArea and update all form renderers to use it, while hardening related JavaScript helpers and test infrastructure.

New Features:

  • Add XoopsDhtmlToolbar as a single source of truth for DHTML editor toolbar markup across Legacy, Bootstrap3/4/5, and Tailwind form renderers.
  • Provide dedicated toolbar CSS and JS assets to render and manage dropdowns in a framework-neutral way.
  • Extend test bootstrap with YouTube-related language constants needed by TextSanitizer extensions.

Enhancements:

  • Refactor all DHTML textarea form renderers to delegate toolbar construction to the shared XoopsDhtmlToolbar while keeping protected helper methods for backward compatibility.
  • Improve JavaScript helpers for the DHTML editor (xoopsCodeText, xoopsSetElementAttribute, xoopsMakeStyle) to avoid null dereferences, remove eval-based dispatch, and ensure useful behavior even without text selection.
  • Add unit tests validating XoopsDhtmlToolbar behavior, renderer delegation, TextSanitizer extension integration, and stylesheet injection.
  • Ensure LocaleRegistry test runs are isolated by purging its cache snapshot at test bootstrap so results are consistent across machines.

Tests:

  • Add XoopsDhtmlToolbarTest to verify identical toolbar output across all form renderers, preload event behavior, TextSanitizer integration, global overrides, check-length handling, and stylesheet injection.
  • Update tests bootstrap to define missing editor-related constants and clear LocaleRegistry cache for deterministic locale handling.

Summary by Gitar

  • Editor Toolbar:
    • Extracted a shared XoopsDhtmlToolbar class to unify editor markup across all renderers
    • Added framework-neutral CSS styling and JavaScript behavior for native <details> dropdowns
    • Repaired no-selection actions and modernised legacy JavaScript helpers in formdhtmltextarea.js
  • Testing:
    • Added comprehensive unit tests in XoopsDhtmlToolbarTest.php to verify renderer output parity and event contracts

This will update automatically on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced standardized DHTML editor toolbar with code formatting, typography controls, alignment options, color selection, and character-length checking.
  • Bug Fixes

    • Enhanced HTML and JavaScript escaping in form elements and media extensions to prevent injection vulnerabilities.
  • Refactor

    • Consolidated toolbar rendering across all form renderers for consistency and maintainability.
  • Tests

    • Added comprehensive test coverage for toolbar functionality, escaping mechanisms, and form rendering consistency.

mambax7 added 2 commits August 1, 2026 04:32
Clicking Size, Font, Colour or B/I/U/S with no text selected did nothing at
all. The no-selection branch called setVisible() on an element with the id
held in $_hiddenText (default xoopsHiddenText), but no renderer has ever
emitted such an element, so xoopsGetElementById() returned null and the
handler threw a TypeError before it could act.

For xoopsSetElementAttribute() that branch was dead twice over: besides the
missing element, it eval'd a setElementSize/Font/Color() helper and no
setElement* function exists in the file. It is removed, so with no selection
the tag pair is simply inserted at the caret, which is what clicking the
button asks for. xoopsMakeStyle() keeps its branch for any custom renderer
that does emit the element, but only takes it when the element and the
handler both genuinely exist, and otherwise falls through to inserting the
pair.

Both eval() calls are gone with it. The remaining dispatch is an explicit map
of the four style helpers, which is all the callers ever pass.

xoopsGetSelect() returns null on browsers without a selection API, so the
selection is normalised to an empty string before use: without that, removing
the old length guard would have inserted the literal text "null" between the
tags.

xoopsCodeText() is a leftover from an older toolbar layout that had a
separate "add text" box. Nothing in core or any bundled extension calls it and
no renderer emits the elements it reads, so every path through it dereferenced
null. It is kept, because it is a global a third-party module could still
call, but now returns harmlessly instead of throwing.
The same editor showed a different toolbar in the control panel than on the
front end, and different again between front-end themes. The cause was not
CSS: renderFormDhtmlTAXoopsCode() and renderFormDhtmlTATypography() were
independently hand-written HTML in five renderers, and no admin theme selects
a renderer, so the control panel silently fell back to the Legacy one whose
Bootstrap class names no admin theme defines.

XoopsDhtmlToolbar now produces the toolbar once and all five renderers
delegate to it. Only renderFormDhtmlTextArea() is on XoopsFormRendererInterface;
the other two are protected helpers, so they are kept as thin delegates for any
third-party subclass that calls or overrides them. Each renderer still supplies
its own chrome -- the textarea, the preview fieldset and the script loader.

The markup is framework-neutral with its own xo-edtb-* classes and a
self-contained stylesheet, because the admin themes load no CSS framework at
all; every colour, radius and size is a custom property so a theme can restyle
it without touching markup. Dropdowns are native <details>, grouped per
textarea with name= so opening one closes the others, with a small script for
older engines plus click-outside and Escape handling. The typography row is
emitted server-side rather than written by document.write, and the colour list
is a curated palette in a class constant instead of 216 entries generated by a
nested loop. role="toolbar" and role="group" are now present, which only the
Bootstrap 5 renderer had.

Four contracts are preserved deliberately. The codeicon preload event still
fires with $code by reference at the same point, so modules can keep appending
buttons. TextSanitizer extensions keep their [$html, $js] return shape, and the
three divergent rewrites of their hardcoded btn-default classes become one.
The global JS function names are untouched, since extensions call them by name.
XoopsFormDhtmlTextArea's public surface is unchanged.

The stylesheet also restores resize:vertical on the editor's textarea: the
default and transition admin themes ship a universal `* { resize:none }` reset
which stripped the grip, so the control panel's editor could not be resized
while the front end's could.

A test asserts the substantive property: all five renderers emit an identical
toolbar for the same element.
Copilot AI review requested due to automatic review settings August 1, 2026 09:16
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 704ead65-bb36-4a5b-aa0a-cef1449ede98

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 82.86% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: introducing a shared DHTML toolbar.

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.

@sourcery-ai

sourcery-ai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce a shared, framework-neutral XoopsDhtmlToolbar for the DHTML textarea editor and refactor all form renderers and JS helpers to use it consistently, with supporting CSS/JS assets and unit tests, plus a small test bootstrap fix for locale registry and constants.

Sequence diagram for rendering DHTML textarea with shared toolbar

sequenceDiagram
    participant Controller
    participant Renderer as XoopsFormRenderer* 
    participant Toolbar as XoopsDhtmlToolbar
    participant Element as XoopsFormDhtmlTextArea

    Controller->>Renderer: renderFormDhtmlTextArea(Element)
    Renderer->>Toolbar: new XoopsDhtmlToolbar()
    Renderer->>Toolbar: render(Element)
    activate Toolbar
    Toolbar->>Toolbar: injectStylesheet()
    Toolbar->>Toolbar: renderCodeButtons(Element)
    Toolbar->>Toolbar: renderTypography(Element)
    Toolbar->>Toolbar: renderCheckLength(Element)
    Toolbar-->>Renderer: toolbarHtml
    deactivate Toolbar

    Renderer->>Renderer: build <textarea> markup
    Renderer-->>Controller: toolbarHtml + textareaHtml
Loading

File-Level Changes

Change Details Files
Refactor all form renderers to delegate toolbar rendering to the new shared XoopsDhtmlToolbar instead of duplicating markup per framework.
  • Require XoopsDhtmlToolbar in Legacy, Bootstrap3/4/5, and Tailwind renderer classes.
  • Replace inline xoopscode, typography, and length-check HTML with a single toolbar->render($element) call in renderFormDhtmlTextArea().
  • Convert existing protected renderFormDhtmlTAXoopsCode()/renderFormDhtmlTATypography() methods into thin delegates that call XoopsDhtmlToolbar->renderCodeButtons()/renderTypography()/renderCheckLength().
  • Preserve protected methods so third-party subclasses can still override or call them.
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
Add the shared, framework-neutral XoopsDhtmlToolbar implementation with support for extensions, preload codeicon events, and stylesheet/script injection.
  • Implement XoopsDhtmlToolbar with render(), renderCodeButtons(), renderTypography(), and renderCheckLength() APIs.
  • Centralize TextSanitizer extension button class rewriting to neutral xo-edtb-* classes and append extension JS to the element.
  • Emit codeicon preload event with the code row passed by reference to preserve third-party button injection contract.
  • Implement framework-neutral dropdowns via
    / and toolbar.css/toolbar.js assets, including mutual exclusivity and accessibility behaviours.
  • Inject toolbar.css and toolbar.js once per request via xoTheme->addStylesheet/addScript or inline /<script> when no theme is present.
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
Harden legacy DHTML editor JS helpers and remove dead eval/document.write paths while preserving external API surface.
  • Update xoopsCodeText() to guard against missing DOM elements and return early instead of throwing.
  • Update xoopsSetElementAttribute() to handle null selections and always insert a tag pair, replacing dead setVisible/setElement* eval path.
  • Introduce xoopsStylePreviewFuncs dispatch table and update xoopsMakeStyle() to avoid eval, guard missing elements, and insert tags when no preview element exists.
  • Ensure xoopsMakeStyle() and xoopsSetElementAttribute() treat null selection as empty string to avoid TypeErrors.
htdocs/include/formdhtmltextarea.js
Improve test bootstrap to define missing form constants and stabilise locale registry cache across test runs.
  • Define _XOOPS_FORM_ALT_ENTERHEIGHT and _XOOPS_FORM_ALTYOUTUBE constants used by youtube extension tests.
  • Add purgeCache() call on Xmf\I18n\Xoops\Registry\LocaleRegistry to clear locale snapshot under XOOPS_VAR_PATH before tests run, avoiding cross-contamination from live sites.
tests/bootstrap.php
Add unit tests to validate that all renderers use the shared toolbar and that toolbar behaviour and contracts are correct.
  • Create XoopsDhtmlToolbarTest covering identical toolbar output across Legacy, Bootstrap3/4/5, and Tailwind renderers.
  • Test delegation to XoopsDhtmlToolbar, codeicon preload event firing and by-reference modification, and TextSanitizer extension button class rewriting and JS injection.
  • Validate absence of document.write/eval in toolbar output, honouring of global formtextdhtml_sizes/fonts overrides, and correctness of check-length button maxlength handling.
  • Test stylesheet injection guard so multiple editors only emit toolbar.css/toolbar.js once.
tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gitar-bot

gitar-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.65385% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.12%. Comparing base (d2b5bde) to head (f7d13a9).
⚠️ Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
...xoopsform/renderer/XoopsFormRendererBootstrap3.php 69.23% 4 Missing ⚠️
...ass/xoopsform/renderer/XoopsFormRendererLegacy.php 69.23% 4 Missing ⚠️
...xoopsform/renderer/XoopsFormRendererBootstrap4.php 75.00% 3 Missing ⚠️
...xoopsform/renderer/XoopsFormRendererBootstrap5.php 75.00% 3 Missing ⚠️
...s/xoopsform/renderer/XoopsFormRendererTailwind.php 57.14% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #151      +/-   ##
============================================
+ Coverage     19.29%   20.12%   +0.83%     
+ Complexity     8227     8200      -27     
============================================
  Files           672      673       +1     
  Lines         44266    43875     -391     
============================================
+ Hits           8539     8832     +293     
+ Misses        35727    35043     -684     

☔ View full report in Codecov by Harness.
📢 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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In XoopsDhtmlToolbar::renderCodeButtons() the imagemanager and smilies URLs now use $element->getName() directly without rawurlencode(), whereas the Tailwind renderer previously encoded the target ID; consider restoring URL encoding to avoid issues when textarea IDs contain characters that are not URL-safe.
  • Each renderer currently creates a new XoopsDhtmlToolbar instance on every renderFormDhtmlTextArea() call; if these toolbars become heavier over time, you may want to share a single instance (e.g. via a static accessor) to avoid repeated construction and keep renderer wiring lean.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In XoopsDhtmlToolbar::renderCodeButtons() the imagemanager and smilies URLs now use $element->getName() directly without rawurlencode(), whereas the Tailwind renderer previously encoded the target ID; consider restoring URL encoding to avoid issues when textarea IDs contain characters that are not URL-safe.
- Each renderer currently creates a new XoopsDhtmlToolbar instance on every renderFormDhtmlTextArea() call; if these toolbars become heavier over time, you may want to share a single instance (e.g. via a static accessor) to avoid repeated construction and keep renderer wiring lean.

## Individual Comments

### Comment 1
<location path="htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php" line_range="153-154" />
<code_context>
+        $textareaId = $element->getName();
+        $btn        = self::BTN_SM;
+
+        $code  = '<a name="moresmiley"></a>';
+        $code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';
+        $code .= $this->button($btn, $this->jsCall('xoopsCodeUrl', [$textareaId, _ENTERURL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_URL, 'fa-solid fa-link');
+        $code .= $this->button($btn, $this->jsCall('xoopsCodeEmail', [$textareaId, _ENTEREMAIL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_EMAIL, 'fa-solid fa-envelope');
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The aria-label for the code button group is tied to the image alt text, which is misleading for screen readers.

The group’s aria-label `_XOOPS_FORM_ALT_IMG` doesn’t match its contents (URL, email, image, smilies, extension, code, quote), so screen readers treat the whole group as image-related. Please use a more generic label (e.g. a new constant like `_XOOPS_FORM_ARIA_CODEBAR`) or remove the group label and rely on the individual button labels instead.

Suggested implementation:

```
        $code  = '<a name="moresmiley"></a>';
        $code .= '<div class="' . self::GROUP_CLASS . '" role="group">';

```

If you prefer a descriptive group label instead of relying solely on individual button labels, you can:
1. Define a new language constant (e.g. `_XOOPS_FORM_ARIA_CODEBAR`) in the appropriate language file.
2. Replace the edited line with:  
   `$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ARIA_CODEBAR . '">';`
This keeps the toolbar accessible while making the label semantically accurate.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +153 to +154
$code = '<a name="moresmiley"></a>';
$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): The aria-label for the code button group is tied to the image alt text, which is misleading for screen readers.

The group’s aria-label _XOOPS_FORM_ALT_IMG doesn’t match its contents (URL, email, image, smilies, extension, code, quote), so screen readers treat the whole group as image-related. Please use a more generic label (e.g. a new constant like _XOOPS_FORM_ARIA_CODEBAR) or remove the group label and rely on the individual button labels instead.

Suggested implementation:

        $code  = '<a name="moresmiley"></a>';
        $code .= '<div class="' . self::GROUP_CLASS . '" role="group">';

If you prefer a descriptive group label instead of relying solely on individual button labels, you can:

  1. Define a new language constant (e.g. _XOOPS_FORM_ARIA_CODEBAR) in the appropriate language file.
  2. Replace the edited line with:
    $code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ARIA_CODEBAR . '">';
    This keeps the toolbar accessible while making the label semantically accurate.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 introduces a shared, framework-neutral DHTML editor toolbar (XoopsDhtmlToolbar) and updates all XOOPS form renderers to delegate toolbar markup generation to it, while also modernizing legacy JavaScript helpers and expanding unit test coverage around toolbar behavior and extension integration.

Changes:

  • Add XoopsDhtmlToolbar as the single source of truth for DHTML toolbar markup (buttons, typography dropdowns, check-length) and shared assets (CSS/JS).
  • Refactor Legacy/Bootstrap3/4/5/Tailwind renderers to delegate toolbar construction to the shared class (keeping protected helpers for compatibility).
  • Improve formdhtmltextarea.js helpers to avoid null dereferences and remove eval(), plus add tests and test bootstrap constants/cache cleanup.

Reviewed changes

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

Show a summary per file
File Description
tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php Adds unit coverage validating renderer parity, preload event behavior, extension integration, and stylesheet injection.
tests/bootstrap.php Defines missing editor-related language constants for tests and purges LocaleRegistry cache for deterministic runs.
htdocs/include/formdhtmltextarea.js Hardens legacy editor JS helpers (null-safety, removes eval-based dispatch, improves no-selection behavior).
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php New shared toolbar implementation (markup generation, extension button class rewrite, stylesheet/script injection).
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js Adds framework-neutral <details> dropdown behavior (mutual exclusivity, click-outside, Escape handling).
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css Adds framework-neutral toolbar styling and targeted CSS fixes for admin-theme resets.
Suppressed comments (2)

htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php:208

  • renderTypography() also uses $element->getName() when generating JS handlers. Since getName() HTML-escapes quotes, the string passed into xoopsSetElementAttribute() can diverge from the real textarea id. Use the raw name via getName(false).
    public function renderTypography(XoopsFormDhtmlTextArea $element): string
    {
        $textareaId = $element->getName();
        $hiddenText = (string) $element->_hiddenText;

htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php:253

  • renderCheckLength() passes $element->getName() into the JS call builder. As with the other toolbar rows, this can pass an HTML-escaped id into JavaScript and fail to find the textarea. Prefer the raw name (getName(false)) for JS arguments.
            $maxlength = (int) $element->configs['maxlength'];
        }

        $onclick = $this->jsCall('XoopsCheckLength', [$element->getName(), (string) $maxlength, _XOOPS_FORM_ALT_LENGTH, _XOOPS_FORM_ALT_LENGTH_MAX]);

Comment on lines +72 to +78
private const RENDERER_CLASSES = [
'XoopsFormRendererLegacy',
'XoopsFormRendererBootstrap3',
'XoopsFormRendererBootstrap4',
'XoopsFormRendererBootstrap5',
'XoopsFormRendererTailwind',
];
Comment on lines +357 to +369
protected function jsCall(string $fn, array $args): string
{
$parts = [];
foreach ($args as $arg) {
if (is_int($arg) || is_float($arg)) {
$parts[] = (string) $arg;
} else {
$parts[] = '"' . htmlspecialchars((string) $arg, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
}

return $fn . '(' . implode(', ', $parts) . ');';
}
Comment on lines +150 to +159
$textareaId = $element->getName();
$btn = self::BTN_SM;

$code = '<a name="moresmiley"></a>';
$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';
$code .= $this->button($btn, $this->jsCall('xoopsCodeUrl', [$textareaId, _ENTERURL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_URL, 'fa-solid fa-link');
$code .= $this->button($btn, $this->jsCall('xoopsCodeEmail', [$textareaId, _ENTEREMAIL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_EMAIL, 'fa-solid fa-envelope');
$code .= $this->button($btn, $this->jsCall('xoopsCodeImg', [$textareaId, _ENTERIMGURL, _ENTERIMGPOS, _IMGPOSRORL, _ERRORIMGPOS, _XOOPS_FORM_ALT_ENTERWIDTH]), _XOOPS_FORM_ALT_IMG, 'fa-solid fa-file-image');
$code .= $this->button($btn, $this->jsCall('openWithSelfMain', [XOOPS_URL . '/imagemanager.php?target=' . $textareaId, 'imgmanager', 400, 430]), _XOOPS_FORM_ALT_IMAGE, 'fa-solid fa-file-image', '<span style="font-size:75%;"> Manager</span>');
$code .= $this->button($btn, $this->jsCall('openWithSelfMain', [XOOPS_URL . '/misc.php?action=showpopups&type=smilies&target=' . $textareaId, 'smilies', 300, 475]), _XOOPS_FORM_ALT_SMILEY, 'fa-solid fa-face-smile');

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php (1)

485-495: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused Tailwind toolbar helper.

renderEditorButton(), BTN_NEUTRAL_SM, and DROPDOWN_MENU_CLS are not called inside XoopsFormRendererTailwind.php; the xoopscode and typography renderers delegate to XoopsDhtmlToolbar. Remove this dead protected helper plus the two private constants, or mark it deprecated if intentional for downstream subclasses.

🤖 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/class/xoopsform/renderer/XoopsFormRendererTailwind.php` around lines
485 - 495, Remove the unused renderEditorButton() helper from
XoopsFormRendererTailwind along with the private constants BTN_NEUTRAL_SM and
DROPDOWN_MENU_CLS. Do not alter the xoopscode or typography renderer delegation
through XoopsDhtmlToolbar.
🤖 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/class/xoopseditor/dhtmltextarea/assets/toolbar.css`:
- Around line 19-37: Move the default custom-property declarations from only
`.xo-edtb-toolbar` to `:root`, while retaining the `.xo-edtb-toolbar` block for
toolbar-specific overrides. Ensure standalone `.xo-edtb-group` rows returned by
`renderFormDhtmlTAXoopsCode()` and `renderFormDhtmlTATypography()` resolve the
button sizing, colors, and border variables without a toolbar ancestor.
- Around line 81-91: Add explicit :focus-visible styles for both .xo-edtb-btn
and .xo-edtb-menu-item, providing a clear visible focus indicator that remains
effective despite the admin theme reset. Keep the existing hover, active, and
sizing rules unchanged.

In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js`:
- Around line 58-70: Update the Escape key handler in the document keydown
listener to restore focus only for the open dropdown that currently contains the
active element. Continue closing all open dropdowns, but avoid moving focus when
typing outside a dropdown and ensure only the focused dropdown’s summary
receives focus.

In `@htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php`:
- Around line 212-230: Escape every language-derived aria-label in
XoopsDhtmlToolbar, including the existing call sites near the toolbar rendering
logic and the three group labels shown here, using a small shared esc helper
that applies htmlspecialchars with ENT_QUOTES | ENT_HTML5 and UTF-8. Update only
these aria-label interpolations; keep button() and dropdown() handling
unchanged.
- Around line 209-215: In the toolbar-building method, validate and normalize
both global overrides before use: accept only arrays for formtextdhtml_sizes and
formtextdhtml_fonts, otherwise use safe defaults, and ensure an empty sizes
array does not render an unusable Size dropdown. Keep font handling compatible
with array_combine by passing only a valid non-empty font list, using the
existing default-font symbol where needed.

In `@htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php`:
- Around line 309-313: Choose one consistent contract for
renderFormDhtmlTATypography() and apply it to all five renderers:
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php#L309-L313,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php#L314-L318,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php#L315-L319,
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php#L224-L228, and
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php#L524-L528. Either
remove renderCheckLength() and provide a separate renderFormDhtmlTACheckLength()
helper, or retain the combined output and update every method docblock to
document both controls; keep the Tailwind change aligned with the dead-helper
cleanup at Line 485.

In `@tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php`:
- Around line 290-299: Make
testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage deterministic by
explicitly controlling the $GLOBALS['xoTheme'] state, preferably clearing it in
setUp() or at the start of the test. Preserve the existing assertions for
first-render injection and second-render suppression.
- Around line 262-265: Update testCheckLengthButtonCarriesConfiguredMaxlength
and the XoopsFormDhtmlTextArea configuration path to avoid dynamically assigning
configs; declare a typed public array $configs initialized to [] on the
appropriate class, or reuse an existing configuration accessor, while preserving
the test’s configured maxlength value.

---

Outside diff comments:
In `@htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php`:
- Around line 485-495: Remove the unused renderEditorButton() helper from
XoopsFormRendererTailwind along with the private constants BTN_NEUTRAL_SM and
DROPDOWN_MENU_CLS. Do not alter the xoopscode or typography renderer delegation
through XoopsDhtmlToolbar.
🪄 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 Plus

Run ID: fb047ae1-dd84-44c6-bfb8-b2284a65f624

📥 Commits

Reviewing files that changed from the base of the PR and between ce1531b and 4ce640f.

📒 Files selected for processing (11)
  • htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
  • htdocs/include/formdhtmltextarea.js
  • tests/bootstrap.php
  • tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Comment thread htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
Comment thread htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
Comment thread htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
Comment thread htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php Outdated
Comment thread htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php Outdated
Comment thread htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
Comment thread tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php
Comment thread tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php
mambax7 added 3 commits August 3, 2026 00:57
Renderers wrote element values straight into quoted attributes and textarea bodies. All five now route values through a shared XoopsFormRendererValueEscapeTrait. Toolbar and TextSanitizer extension handlers build their JavaScript arguments with json_encode instead of htmlspecialchars, which is the correct encoder for a JS string literal inside an HTML attribute. The extension-authoring sample in readme.txt is updated to match.
Three copies of core renderers had accumulated under the TinyMCE image-manager plugins and were installed as the global renderer, so those screens did not receive core renderer changes. They were stale snapshots with no local modifications; removed, and the endpoints now xoops_load() the core class. Separately, the image category create, update and delete handlers validated the CSRF token but not the caller's permission - the admin check applied only to which controls were rendered. Guards added to all five handlers.
…rer uniqueness

Pinned methods keep their documented text-context exclusion but are now exercised with payloads that cannot express themselves in element text, so their attribute sites stay covered. Adds a per-element attribute-name-set assertion, per-extension coverage that bypasses the config gate, an onclick delimiter assertion, and a test that each renderer class is declared exactly once.
Copilot AI review requested due to automatic review settings August 3, 2026 06:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 28 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (1)

tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php:196

  • The containment assertions for element/script/img counts are inverted: as written they pass when the hostile render has more nodes than the baseline (i.e., the injection case) and fail when it has fewer. This makes the test ineffective at catching breakouts that add elements/scripts/images.
                self::assertLessThanOrEqual($baseline['elements'], $shape['elements'], "An element was injected by $where");
                self::assertLessThanOrEqual($baseline['scripts'], $shape['scripts'], "A <script> was injected by $where");
                self::assertLessThanOrEqual($baseline['images'], $shape['images'], "An <img> was injected by $where");

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

The PR consolidates DHTML editor toolbar generation into a shared, framework-neutral implementation and updates all supported form renderers to delegate to it.

  • Adds shared toolbar markup, styling, dropdown behavior, and asset registration.
  • Hardens JavaScript argument encoding and DHTML helper behavior.
  • Replaces TinyMCE image-manager renderer snapshots with canonical core renderer loading.
  • Adds renderer, escaping, extension-integration, uniqueness, and toolbar tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php Introduces the shared toolbar implementation, context-aware escaping, extension integration, and one-time asset registration.
htdocs/include/formdhtmltextarea.js Replaces eval-based style dispatch and hardens no-selection and missing-element behavior.
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js Adds delegated dropdown exclusivity, outside-click handling, and Escape-key behavior.
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css Adds framework-neutral toolbar and dropdown styling with compatibility rules for existing themes.
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php Delegates legacy DHTML toolbar rendering to the shared implementation while retaining compatibility helpers.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php Delegates Bootstrap 5 DHTML toolbar generation to the shared implementation.
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php Delegates Tailwind DHTML toolbar generation to the shared implementation.
htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php Loads the canonical core Bootstrap renderer instead of a plugin-local snapshot.
tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php Covers shared output parity, delegation, preload integration, length handling, and asset injection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    F[XoopsFormDhtmlTextArea] --> R[Selected form renderer]
    R --> T[XoopsDhtmlToolbar]
    T --> C[Code and extension buttons]
    T --> Y[Typography controls]
    T --> L[Length check]
    T --> A[Shared CSS and JavaScript assets]
Loading

Reviews (4): Last reviewed commit: "fix(textsanitizer): degrade invalid UTF-..." | Re-trigger Greptile

Address review findings on the shared dhtml toolbar:

- Validate the shape of the formtextdhtml_fonts and formtextdhtml_sizes
  globals before use. Both are documented override points set by module
  code; a non-array value previously reached array_combine() (TypeError,
  killing the whole form render) or the dropdown foreach. Non-scalar
  font entries are dropped and an empty list falls back to the defaults.
- Escape the concatenated language constants that land inside the four
  group aria-label attributes through a shared esc() helper, matching
  the escaping button() and dropdown() already apply to their titles.
- Declare the configs property on XoopsFormDhtmlTextArea. Callers set
  it directly for the check-length maxlength, which is deprecated
  dynamic-property creation on PHP 8.2+ when undeclared.
- toolbar.js: on Escape, still close every open dropdown but return
  focus only to the toggle of the dropdown that contained the active
  element, so Escape no longer pulls focus out of the textarea.
- Document that renderFormDhtmlTATypography() returns the typography
  groups plus the check-length button in all five renderers - the same
  combined output the method produced before the toolbar extraction.
- Pin $GLOBALS['xoTheme'] absent in the toolbar test setUp() (restored
  in tearDown()) so the stylesheet-injection assertions do not depend
  on test execution order.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 29 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php:196

  • The containment assertions are reversed: as written they only fail when the hostile render has fewer nodes than the benign baseline, and they will not fail when an injection adds elements/scripts/images. The assertions should ensure the hostile shape does not exceed the baseline counts.
                self::assertLessThanOrEqual($baseline['elements'], $shape['elements'], "An element was injected by $where");
                self::assertLessThanOrEqual($baseline['scripts'], $shape['scripts'], "A <script> was injected by $where");
                self::assertLessThanOrEqual($baseline['images'], $shape['images'], "An <img> was injected by $where");

tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php:398

  • libxml_use_internal_errors(true) changes a global setting and isn't restored, which can leak into other tests and make later libxml warnings/errors harder to diagnose. Capture the previous value and restore it before returning.
        libxml_use_internal_errors(true);
        $doc->loadHTML('<!doctype html><meta charset="utf-8"><body>' . $html . '</body>', LIBXML_NOERROR);
        libxml_clear_errors();

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php (1)

407-415: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle JSON encoding failures without aborting form rendering.

json_encode() with JSON_THROW_ON_ERROR throws JsonException for malformed UTF-8 in string arguments such as toolbar labels. Catch \Throwable near the encoding step, emit E_USER_WARNING, and append a safe empty string so toolbar rendering can continue.

🤖 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/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php` around lines
407 - 415, Update jsCall’s argument-encoding branch to catch \Throwable from
json_encode, emit an E_USER_WARNING, and append a safe empty-string argument
when encoding fails. Preserve numeric handling and successful JSON encoding so
malformed toolbar labels do not abort form rendering.

Source: Coding guidelines

🤖 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/class/textsanitizer/mms/mms.php`:
- Around line 39-45: Escape each localized title before concatenating it into
the HTML title attribute: in htdocs/class/textsanitizer/mms/mms.php lines 39-45,
apply htmlspecialchars with ENT_QUOTES | ENT_SUBSTITUTE and UTF-8 to
_XOOPS_FORM_ALTMMS; make the same change for _XOOPS_FORM_ALTMP3 in
htdocs/class/textsanitizer/mp3/mp3.php lines 34-36, _XOOPS_FORM_ALTRTSP in
htdocs/class/textsanitizer/rtsp/rtsp.php lines 39-45, _XOOPS_FORM_ALT_SOUNDCLOUD
in htdocs/class/textsanitizer/soundcloud/soundcloud.php lines 18-22,
_XOOPS_FORM_ALTWIKI in htdocs/class/textsanitizer/wiki/wiki.php lines 38-42,
_XOOPS_FORM_ALTWMP in htdocs/class/textsanitizer/wmp/wmp.php lines 39-45, and
_XOOPS_FORM_ALTYOUTUBE in htdocs/class/textsanitizer/youtube/youtube.php lines
31-37.

In `@htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php`:
- Around line 226-239: Update the toolbar rendering around the $sizes
initialization and the size dropdown call in the toolbar method so the Size
dropdown is appended only when $sizes contains valid options. Preserve rendering
of the Font and Color dropdowns regardless of whether formtextdhtml_sizes is
absent or empty.
- Around line 150-169: The compatibility path around renderCodeButtons() and
renderTypography() must ensure toolbar.css and toolbar.js are loaded even when
render() is bypassed. Update the retained protected helpers or add an explicit
shared asset-rendering method, and invoke it from those helper paths without
duplicating asset registration when render() is used.

In `@htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php`:
- Around line 46-48: Escape the button text using escapeElementValue() instead
of appending getValue() directly in the Bootstrap 4 renderer’s button output.
Apply the same change in
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php lines 47-49,
ensuring both renderers safely encode button values while preserving their
existing title and value attributes.

In `@tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php`:
- Around line 121-124: Escape the button caption derived from element values in
Bootstrap 4 and Bootstrap 5 renderFormButton and renderFormButtonTray
implementations. Then remove those four method entries from
KNOWN_TEXT_CONTEXT_GAPS so the containment test covers them.
- Around line 392-399: Update the parse method to capture the previous libxml
internal-error setting before calling libxml_use_internal_errors(true), then
restore that saved setting after libxml_clear_errors() and before returning the
DOMDocument.

In `@tests/unit/htdocs/class/xoopsform/XoopsFormRendererUniquenessTest.php`:
- Around line 97-102: The phpFiles scan in XoopsFormRendererUniquenessTest must
inspect every PHP file for renderer declarations; remove the filename-based
str_starts_with filter in the foreach loop, or replace it with a content-based
prefilter that still detects declarations such as xoopsformrenderer.php.

---

Outside diff comments:
In `@htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php`:
- Around line 407-415: Update jsCall’s argument-encoding branch to catch
\Throwable from json_encode, emit an E_USER_WARNING, and append a safe
empty-string argument when encoding fails. Preserve numeric handling and
successful JSON encoding so malformed toolbar labels do not abort form
rendering.
🪄 Autofix

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 Plus

Run ID: e9fdba3c-ab72-46a4-803f-2d3e06487815

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce640f and f2adce6.

📒 Files selected for processing (28)
  • htdocs/class/textsanitizer/mms/mms.php
  • htdocs/class/textsanitizer/mp3/mp3.php
  • htdocs/class/textsanitizer/readme.txt
  • htdocs/class/textsanitizer/rtsp/rtsp.php
  • htdocs/class/textsanitizer/soundcloud/soundcloud.php
  • htdocs/class/textsanitizer/wiki/wiki.php
  • htdocs/class/textsanitizer/wmp/wmp.php
  • htdocs/class/textsanitizer/youtube/youtube.php
  • htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
  • htdocs/class/xoopseditor/tinymce5/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopseditor/tinymce5/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php
  • htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap5.php
  • htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/xoopsimagemanager.php
  • htdocs/class/xoopsform/formdhtmltextarea.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererValueEscapeTrait.php
  • tests/bootstrap.php
  • tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarExtensionEscapingTest.php
  • tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php
  • tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php
  • tests/unit/htdocs/class/xoopsform/XoopsFormRendererUniquenessTest.php
💤 Files with no reviewable changes (3)
  • htdocs/class/xoopseditor/tinymce5/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap5.php
  • htdocs/class/xoopseditor/tinymce7/js/tinymce/plugins/xoopsimagemanager/XoopsFormRendererBootstrap4.php

Comment thread htdocs/class/textsanitizer/mms/mms.php
Comment thread htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
Comment thread htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
Comment thread htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
Comment thread tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php
Comment thread tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php
- Escape the localized title attribute in the seven TextSanitizer
  extension buttons (mms, mp3, rtsp, soundcloud, wiki, wmp, youtube);
  the constants were concatenated raw into a single-quoted attribute.
- Omit the Size dropdown when no size options are configured instead of
  rendering a toggle that opens an empty menu; Font and Color still
  render, and the group aria-label drops the size segment accordingly.
- Inject toolbar.css/toolbar.js from renderCodeButtons() and
  renderTypography() as well as render(), so a renderer subclass that
  composes the rows directly still gets working assets; the existing
  static guard keeps the injection single-shot per request.
- jsCall(): degrade a json_encode() failure (malformed UTF-8 in a
  translated label) to a warning plus an empty-string argument instead
  of letting the exception abort the whole form render.
- Tests: restore the previous libxml internal-errors setting in the
  escaping test's parse() helper; switch the renderer-uniqueness scan
  to a content prefilter so declarations in differently-named files
  are still found; pin asset state in the byte-identity toolbar tests;
  cover the direct-helper asset injection and the omitted Size
  dropdown.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 29 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (8)

tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php:196

  • The containment assertions are inverted: this currently asserts the hostile render has at least as many elements/scripts/images as the benign baseline, which will not fail when injection adds nodes. To enforce containment, the hostile shape must be <= the baseline shape for counts (elements/scripts/images).
                self::assertLessThanOrEqual($baseline['elements'], $shape['elements'], "An element was injected by $where");
                self::assertLessThanOrEqual($baseline['scripts'], $shape['scripts'], "A <script> was injected by $where");
                self::assertLessThanOrEqual($baseline['images'], $shape['images'], "An <img> was injected by $where");

htdocs/class/textsanitizer/youtube/youtube.php:36

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeYoutube("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTERYOUTUBEURL, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERHEIGHT, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERWIDTH, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALTYOUTUBE, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

htdocs/class/textsanitizer/mp3/mp3.php:36

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $buttonHtml = "<button type='button' class='btn btn-default' onclick='xoopsCodeMp3("
                      . json_encode((string) $textarea_id, $jsonFlags) . ");' title='"
                      . htmlspecialchars(_XOOPS_FORM_ALTMP3, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "'>"

htdocs/class/textsanitizer/wmp/wmp.php:44

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeWmp("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTERWMPURL, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERHEIGHT, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERWIDTH, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALTWMP, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

htdocs/class/textsanitizer/mms/mms.php:44

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeMms("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTERMMSURL, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERHEIGHT, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERWIDTH, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALTMMS, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

htdocs/class/textsanitizer/rtsp/rtsp.php:44

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeRtsp("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTERRTSPURL, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERHEIGHT, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ALT_ENTERWIDTH, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALTRTSP, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

htdocs/class/textsanitizer/soundcloud/soundcloud.php:21

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeSoundCloud("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTER_SOUNDCLOUD_URL, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALT_SOUNDCLOUD, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

htdocs/class/textsanitizer/wiki/wiki.php:41

  • Using JSON_THROW_ON_ERROR here can make toolbar rendering fatal if any argument contains invalid UTF-8 bytes (e.g., a mis-encoded translation constant). Since this value is used only to safely embed strings into an onclick handler, prefer JSON_INVALID_UTF8_SUBSTITUTE (non-throwing) so the extension degrades safely instead of crashing the request.
        $jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
        $code = "<button type='button' class='btn btn-default btn-sm' onclick='xoopsCodeWiki("
            . json_encode((string) $textarea_id, $jsonFlags) . ","
            . json_encode(_XOOPS_FORM_ENTERWIKITERM, $jsonFlags)
            . ");' onmouseover='style.cursor=\"hand\"' title='" . htmlspecialchars(_XOOPS_FORM_ALTWIKI, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')

…owing

Swap JSON_THROW_ON_ERROR for JSON_INVALID_UTF8_SUBSTITUTE in the seven
extension button builders (mms, mp3, rtsp, soundcloud, wiki, wmp,
youtube) and the readme pattern they follow. The values only feed
string arguments in an onclick handler, so a mis-encoded translation
constant should produce a substituted character in one button, not an
uncaught JsonException that aborts the whole form render. Same failure
class as the jsCall() hardening in the shared toolbar.
@mambax7

mambax7 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

f7d13a9 addresses Copilot's latest suppressed comments (no open threads, so noting here):

JSON_THROW_ON_ERROR in the seven TextSanitizer extension buttons — fixed. All seven (mms, mp3, rtsp, soundcloud, wiki, wmp, youtube) plus the readme pattern now use JSON_INVALID_UTF8_SUBSTITUTE: the values only feed string arguments in an onclick handler, so a mis-encoded translation constant now degrades to a substituted character in one button instead of an uncaught JsonException aborting the whole form render. Same failure class as the jsCall() hardening in 2b4da55.

"Containment assertions are inverted" in XoopsFormRendererEscapingTest — not changed; the claim reads PHPUnit's parameter order backwards. assertLessThanOrEqual($expected, $actual) asserts $actual <= $expected, and the test passes ($baseline, $shape), so it asserts the hostile render's element/script/img counts stay within the benign baseline — exactly the containment direction the comment asks for. An injection that adds nodes makes $shape exceed $baseline and fails the assertion.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 29 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (1)

tests/unit/htdocs/class/xoopsform/XoopsFormRendererEscapingTest.php:196

  • The injection-containment assertions in this test are inverted: assertLessThanOrEqual($baseline, $shape) will pass when the hostile render adds elements/scripts/images (i.e., exactly when injection happened). This makes the suite unable to detect regressions in escaping.

Swap the operands so the hostile shape is required to be <= the benign baseline (no increases).

                self::assertLessThanOrEqual($baseline['elements'], $shape['elements'], "An element was injected by $where");
                self::assertLessThanOrEqual($baseline['scripts'], $shape['scripts'], "A <script> was injected by $where");
                self::assertLessThanOrEqual($baseline['images'], $shape['images'], "An <img> was injected by $where");

@mambax7
mambax7 merged commit 11d30ff into XOOPS:master Aug 7, 2026
14 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