diff --git a/test/js/README.md b/test/js/README.md index 9d7c3b95..c9eae3b9 100644 --- a/test/js/README.md +++ b/test/js/README.md @@ -40,6 +40,7 @@ nothing is listening, so it stays useful in a bare checkout. | `unit/test_style_editor_element_keys.js` | no | `elementKeys()`/`styleRows()`/`positionRows()` from `widgets/style-editor.js`: every `customization.layout` entry gets exactly one row -- paired with its style element through core's `x-layout-key` (so `score` belongs to `score_text`, not a second row), or a position row of its own, leaves included -- since the widget claims the whole `layout` block from the generic fallback renderer | | `unit/test_style_editor_layout_leaf_columns.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only key whose own value is a leaf (no x/y sub-object, e.g. a `show_logo` toggle) gets a self-keyed column instead of a blank, uneditable row | | `unit/test_style_editor_layout_leaf_collision.js` | no | `columnsFor()` from `widgets/style-editor.js`: a layout-only leaf key still gets its own column even when its name collides with an unrelated element's style sub-field or another layout axis's sub-field | +| `unit/test_inline_handler_escaping.js` | no | The store, saved-repository and custom-registry inline `onclick` handlers and the live `window.updateImageList` from `plugins_manager.js`: a registry id, URL or uploaded file name carrying `'`, `"` or entities adds no attributes and reaches the handler intact, and the store's View button opens only http(s) links | | `dom/test_installed_dom.js` | yes | The toolbar in a real DOM: pill/search/sort interaction, the HTMX partial re-swap, and a `getComputedStyle` check that `.filter-pill[data-active]` really matches the emitted markup | | `dom/test_store_dom.js` | yes | Store pagination, per-page, category, tri-state Installed button, and persistence across a re-boot, against the live registry | | `dom/test_no_double_fetch.js` | yes | Loads the **whole** `plugins_manager.js` and counts requests: typing in the store search must filter the cached list, not refetch `/api/v3/plugins/store/list` | diff --git a/test/js/run_all.js b/test/js/run_all.js index bf9ca625..abf0fff7 100755 --- a/test/js/run_all.js +++ b/test/js/run_all.js @@ -18,7 +18,7 @@ const UNIT = ['unit/test_list_filter.js', 'unit/test_render_cards.js', 'unit/test_html_escaping.js', 'unit/test_style_editor_element_keys.js', 'unit/test_style_editor_layout_leaf_columns.js', 'unit/test_style_editor_layout_leaf_collision.js', - 'unit/test_update_all.js']; + 'unit/test_update_all.js', 'unit/test_inline_handler_escaping.js']; const DOM = ['dom/test_installed_dom.js', 'dom/test_store_dom.js', 'dom/test_no_double_fetch.js', 'dom/test_tools_sections.js']; diff --git a/test/js/unit/test_inline_handler_escaping.js b/test/js/unit/test_inline_handler_escaping.js new file mode 100644 index 00000000..984d4299 --- /dev/null +++ b/test/js/unit/test_inline_handler_escaping.js @@ -0,0 +1,215 @@ +// Registry- and upload-supplied strings must not escape the inline handlers +// plugins_manager.js builds for them. +// +// The store, saved-repository and custom-registry renderers wrote +// +// - @@ -4102,9 +4104,9 @@ function renderSavedRepositories(repositories) { return; } - // Helper function to escape for JavaScript strings + // JS string literal for an inline handler; see jsStringAttr const escapeJs = (text) => { - return JSON.stringify(text || ''); + return jsStringAttr(text || ''); }; container.innerHTML = repositories.map(repo => { @@ -4498,9 +4500,9 @@ function renderCustomRegistryPlugins(plugins, registryUrl) { .replace(/'/g, '''); }; - // Helper function to escape for JavaScript strings + // JS string literal for an inline handler; see jsStringAttr const escapeJs = (text) => { - return JSON.stringify(text || ''); + return jsStringAttr(text || ''); }; container.innerHTML = plugins.map(plugin => { @@ -4617,6 +4619,15 @@ function escapeAttribute(text) { .replace(/>/g, '>'); } +// A quoted JS string literal that is safe inside an inline handler attribute +// (onclick='f(${jsStringAttr(id)})' or onclick="..."). JSON.stringify alone +// makes a valid JS string but leaves ' and & untouched, so a registry entry +// id containing ' closed a single-quoted attribute and added its own +// handlers. The browser decodes the entities before the JS is parsed. +function jsStringAttr(value) { + return escapeAttribute(JSON.stringify(value == null ? '' : String(value))); +} + // Format date for display function formatDate(dateString) { if (!dateString) return 'Unknown'; @@ -5104,11 +5115,11 @@ window.updateImageList = function(fieldId, images) { const scheduleSummary = hasSchedule ? (window.getScheduleSummary ? window.getScheduleSummary(imgSchedule) : 'Scheduled') : 'Always shown'; return ` -
+
- ${img.filename || ''} @@ -5116,7 +5127,7 @@ window.updateImageList = function(fieldId, images) {
-

${img.original_filename || img.filename || 'Image'}

+

${escapeHtml(img.original_filename || img.filename || 'Image')}

${window.formatFileSize ? window.formatFileSize(img.size || 0) : (Math.round((img.size || 0) / 1024) + ' KB')} • ${window.formatDate ? window.formatDate(img.uploaded_at) : (img.uploaded_at || '')}

${scheduleSummary} @@ -5125,14 +5136,14 @@ window.updateImageList = function(fieldId, images) {

- +
`; }).join('');