Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# language: php
build:
environment:
php: 8.1.0
php: 8.2
nodes:
tests: true
analysis:
dependencies:
after:
# The following installs the most recent XOOPS CMS version, you might want
# to install a specific release tag or branch instead.
- git clone --depth=1 https://github.com/XOOPS/XoopsCore25
- git clone --depth=1 https://github.com/mambax7/tag
- git clone --depth=1 https://github.com/XOOPS/XoopsCore27
# - git clone --depth=1 https://github.com/mambax7/tag
project_setup:
override: true
tests:
override:
- php-scrutinizer-run --enable-security-analysis
filter:
dependency_paths:
- XoopsCore25/*
- XoopsCore27/*
- tag/*

43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,49 @@ Baseline comparison for **1.6.0**: previous public tree `1.5.1-Final`

---

## [1.7.0 Alpha 1] — 2026-07-30

### Added

- Structured reporting package with typed log fragments, events, severities,
outcomes, operation results, a legacy adapter, and a safe HTML renderer
- `ModuleActionResult::events()`, `messageText()`, and `toOperationResult()` for
HTML-independent report consumption
- `ModuleActionService::operationResult()` and `operationResults()` for callers
adopting `Report\ModuleOperationResult`
- 30 translatable admin-report strings with safe English fallbacks

### Changed

- Install-action reports are now structured data that can be rendered to HTML or
consumed as plain text, CLI output, logs, or JSON
- `ModuleActionResult` is deprecated in favor of `Report\ModuleOperationResult`;
existing `runOne()` and `runMany()` callers remain supported through XOOPS 2.8
- RTL output now isolates counters, versions, statuses, folders, and plan summaries
- Admin report layout uses logical CSS properties, removes module-owned inline
styles, improves long-log scrolling, and avoids duplicate screen-reader labels
- Expanded tests (109 tests / 264 assertions) for hostile log input,
structured-result contracts, translation fallbacks, module-logo path rejection,
and UTF-8 boundary handling

### Fixed

- Invalid UTF-8 caused by byte-based NBSP trimming
- Duplicate failure transcripts in structured results
- Blank pages caused by exceptions while loading a module manifest after an action
- Fatal or unusable admin output from missing, blank, non-scalar, or malformed
language strings
- Incorrect protected-module action wording

### Security

- Replaced direct install-log markup rendering with an escape-once parser that emits
only fixed module-owned markup and drops executable or raw-text element bodies
- Centralized module-logo URL validation and rejection of remote, missing, absolute,
traversal, and otherwise unsafe paths

---

## [1.6.0] — 2026-07-23

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ uninstalling).

| | |
|---|---|
| **Current version** | 1.6.0 Stable (2026-07-23) |
| **Current version** | 1.7.0 Alpha 1 (2026-07-30) |
| **Previous baseline** | 1.5.1 Final (2022-12-15) |
| **License** | GNU GPL 2.0 |
| **Author** | Michael Beck (Mamba) |

Full history: [CHANGELOG.md](CHANGELOG.md) · [docs/changelog.txt](docs/changelog.txt)
Full history: [CHANGELOG.md](CHANGELOG.md) · [docs/changelog.txt](docs/changelog.txt) · [docs/lang_diff.txt](docs/lang_diff.txt)
How-to: [docs/TUTORIAL.md](docs/TUTORIAL.md)

---
Expand Down
2 changes: 1 addition & 1 deletion admin/admin_footer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
$pathIcon32 = Admin::iconUrl('', '32');
//$pathIcon32 = Xmf\Module\Admin::iconUrl('', '32');

echo "<div class='adminfooter'>\n" . " <div style='text-align: center;'>\n" . " <a href='https://xoops.org' rel='external'><img src='{$pathIcon32}/xoopsmicrobutton.gif' alt='XOOPS' title='XOOPS'></a>\n" . " </div>\n" . ' ' . _AM_MODULEADMIN_ADMIN_FOOTER . "\n" . '</div>';
echo "<div class='adminfooter'>\n" . " <div class='installer-footer-center'>\n" . " <a href='https://xoops.org' rel='external'><img src='{$pathIcon32}/xoopsmicrobutton.gif' alt='XOOPS' title='XOOPS'></a>\n" . " </div>\n" . ' ' . _AM_MODULEADMIN_ADMIN_FOOTER . "\n" . '</div>';

xoops_cp_footer();
7 changes: 5 additions & 2 deletions admin/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,15 @@

echo '<div class="installer-dash-card"><h4>' . _AM_MODULEINSTALLER_DASH_SETS . '</h4>';
echo '<div class="num">' . \count($sets) . '</div>';
echo '<p>' . \sprintf(_AM_MODULEINSTALLER_DASH_SETS_COUNT, \count($sets)) . '</p>';
// Isolate the rendered sentence, NOT the numeral: a %d placeholder casts its
// argument to int, so markup passed into it is silently discarded. Without the
// isolate, '%d saved set(s)' renders as "saved set(s) 0" in an RTL admin.
echo '<p><bdi>' . \sprintf(_AM_MODULEINSTALLER_DASH_SETS_COUNT, \count($sets)) . '</bdi></p>';
echo '<p><a href="sets.php">' . _AM_MODULEINSTALLER_DASH_GO_SETS . '</a></p></div>';

echo '<div class="installer-dash-card"><h4>' . _AM_MODULEINSTALLER_DASH_UPDATES . '</h4>';
echo '<div class="num">' . \count($needsUpdate) . '</div>';
echo '<p>' . \sprintf(_AM_MODULEINSTALLER_DASH_UPDATES_COUNT, \count($needsUpdate)) . '</p>';
echo '<p><bdi>' . \sprintf(_AM_MODULEINSTALLER_DASH_UPDATES_COUNT, \count($needsUpdate)) . '</bdi></p>';
echo '<p><a href="update.php?needs_only=1">' . _AM_MODULEINSTALLER_DASH_GO_UPDATE . '</a></p></div>';

echo '<div class="installer-dash-card"><h4>' . _AM_MODULEINSTALLER_DASH_ACTIVE . '</h4>';
Expand Down
44 changes: 30 additions & 14 deletions admin/sets.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ function renderSetList(ModuleSetRepository $repository, ModuleSetResolver $resol
$adminObject->addItemButton(_AM_MODULEINSTALLER_SET_FROM_ACTIVE, 'javascript:document.getElementById("form-from-active").submit();', 'button_ok');
$adminObject->displayButton('left');

echo '<form id="form-from-active" method="post" action="sets.php" style="display:none;">';
echo '<form id="form-from-active" method="post" action="sets.php" class="installer-hidden">';
echo $GLOBALS['xoopsSecurity']->getTokenHTML();
echo '<input type="hidden" name="op" value="create_from_active">';
echo '<input type="hidden" name="name" value="' . \htmlspecialchars(_AM_MODULEINSTALLER_SET_FROM_ACTIVE_DEFAULT, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '">';
Expand Down Expand Up @@ -292,7 +292,7 @@ function renderSetList(ModuleSetRepository $repository, ModuleSetResolver $resol
}
echo '</td>';
echo '<td class="center">' . $set->countModules() . '</td>';
echo '<td class="center">' . ($missing > 0 ? '<span style="color:#c00;font-weight:bold;">' . $missing . '</span>' : '0') . '</td>';
echo '<td class="center">' . ($missing > 0 ? '<span class="installer-log-error">' . $missing . '</span>' : '0') . '</td>';
echo '<td class="center">';
echo '<a href="sets.php?op=apply_form&amp;id=' . \rawurlencode($set->getId()) . '" title="' . \htmlspecialchars(_AM_MODULEINSTALLER_SET_TIP_APPLY, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '">' . _AM_MODULEINSTALLER_SET_APPLY . '</a> | ';
echo '<a href="sets.php?op=edit&amp;id=' . \rawurlencode($set->getId()) . '">' . _EDIT . '</a> | ';
Expand Down Expand Up @@ -392,7 +392,7 @@ function renderSetEditForm(
echo ' <span class="small installer-filter-hint">' . _AM_MODULEINSTALLER_FILTER_HINT . '</span>';
echo '</div>';
echo '</div>';
echo '<p id="installer-filter-empty" class="x2-note confirmMsg" style="display:none;">' . _AM_MODULEINSTALLER_SET_FILTER_NONE . '</p>';
echo '<p id="installer-filter-empty" class="x2-note confirmMsg installer-hidden">' . _AM_MODULEINSTALLER_SET_FILTER_NONE . '</p>';

$onDisk = $catalog->listOnDisk();
// Include missing members so user can uncheck/remove them
Expand All @@ -418,8 +418,9 @@ function renderSetEditForm(
$inSet = isset($selectedMap[$dirname]);
$checked = $inSet ? ' checked' : '';
$rowClass = $class . ' set-mod-row' . ($inSet ? ' installer-row-selected' : '');
$cellBg = $inSet ? 'background-color:var(--installer-selected,#E6EFC2);' : '';
$rowStyle = $inSet ? ' style="background-color:var(--installer-selected,#E6EFC2);"' : '';
// Row and cell colour come from .installer-row-selected in admin.css, which
// declares them with !important for tr and for tr > td alike, so the inline
// copies that used to be built here were redundant AND already overridden.

$label = null !== $info
? \htmlspecialchars($info['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ' <span class="small">(/' . \htmlspecialchars($dirname, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ')</span>'
Expand All @@ -429,7 +430,12 @@ function renderSetEditForm(

$img = '';
if (null !== $info && '' !== $info['image']) {
$img = '<img class="installer-mod-logo" src="' . \XOOPS_URL . '/modules/' . \rawurlencode($info['dirname']) . '/' . \htmlspecialchars($info['image'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" alt="">';
// One logo authority for the whole module: AdminBulkPage::moduleLogoUrl()
// proves the manifest's image is a real file inside the module directory.
$logoUrl = \XoopsModules\Moduleinstaller\AdminBulkPage::moduleLogoUrl((string) $info['dirname'], $info);
$img = null === $logoUrl
? '<span class="installer-mod-logo"></span>'
: '<img class="installer-mod-logo" src="' . \htmlspecialchars($logoUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" alt="">';
}

$dirnameEsc = \htmlspecialchars($dirname, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Expand All @@ -438,16 +444,22 @@ function renderSetEditForm(
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
$tdStyle = $cellBg !== '' ? ' style="' . $cellBg . '"' : '';
echo '<tr class="' . $rowClass . '"' . $rowStyle . ' data-dirname="' . $dirnameEsc . '" data-search="' . $searchBlob . '">';
echo '<td class="center img installer-mod-toggle" title="Toggle selection">' . $img . '</td>';
echo '<td class="installer-mod-toggle installer-mod-desc" title="Toggle selection">' . $label;
echo '<tr class="' . $rowClass . '" data-dirname="' . $dirnameEsc . '" data-search="' . $searchBlob . '">';
// _AM_MODULEINSTALLER_TOGGLE_SELECTION already exists and is used by
// AdminBulkPage; these two were the last places still hardcoding it.
$toggleTitle = \htmlspecialchars(
\XoopsModules\Moduleinstaller\Lang::text('_AM_MODULEINSTALLER_TOGGLE_SELECTION', 'Toggle selection'),
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
echo '<td class="center img installer-mod-toggle" title="' . $toggleTitle . '">' . $img . '</td>';
echo '<td class="installer-mod-toggle installer-mod-desc" title="' . $toggleTitle . '">' . $label;
if (null !== $info && '' !== $info['description']) {
echo '<br><span class="small installer-mod-desc-text">' . \htmlspecialchars($info['description'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
}
echo '</td>';
echo '<td' . $tdStyle . '>' . $statusLabel . '</td>';
echo '<td class="center installer-set-cb-cell"' . $tdStyle . '><input type="checkbox" class="set-module-cb" name="modules[' . $dirnameEsc . ']" value="1"' . $checked . '></td>';
echo '<td>' . $statusLabel . '</td>';
echo '<td class="center installer-set-cb-cell"><input type="checkbox" class="set-module-cb" name="modules[' . $dirnameEsc . ']" value="1"' . $checked . '></td>';
echo '</tr>';
}
echo '</tbody></table></div>';
Expand Down Expand Up @@ -621,14 +633,18 @@ function renderApplyForm(
echo "<div class='x2-note confirmMsg'>" . _AM_MODULEINSTALLER_SET_PREVIEW_EMPTY . '</div>';
} else {
$counts = installer_plan_counts($plan);
echo '<div class="installer-plan-summary">' . \sprintf(
// Five numerals separated by '·' in one sentence: the worst bidi case in the
// module. The counts cannot be wrapped individually — the format string uses
// %1$d..%5$d and a numeric placeholder casts its argument to int, discarding
// any markup — so the rendered sentence is isolated as a unit instead.
echo '<div class="installer-plan-summary"><bdi>' . \sprintf(
_AM_MODULEINSTALLER_SET_PLAN_SUMMARY,
$counts['activate'],
$counts['deactivate'],
$counts['install'],
$counts['uninstall'],
$counts['other']
) . '</div>';
) . '</bdi></div>';
echo '<table class="outer width100" cellspacing="1"><thead><tr>';
echo '<th>' . _AM_MODULEINSTALLER_SET_MODULE . '</th><th>' . _AM_MODULEINSTALLER_SET_ACTION . '</th><th>' . _AM_MODULEINSTALLER_SET_REASON . '</th>';
echo '</tr></thead><tbody>';
Expand Down
Loading
Loading