Skip to content

Commit 1b2e91f

Browse files
gcgoncalvescrivetimihai
authored andcommitted
[FIX][UI]: Replace inline event handlers on Plugins page to survive innerHTML sanitizer (#3288)
* Add event listener for plugin filters Signed-off-by: Gabriel Costa <gabrielcg@proton.me> * fix(tests): clean up plugin filter tests — lint fixes and scrollIntoView polyfill - Remove unused imports (beforeEach) and destructured variables (card1, card2, hookFilter) - Add HTMLElement.prototype.scrollIntoView polyfill for JSDOM environment - Revert unrelated package-lock.json changes Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(ui): prevent keydown handler from swallowing Space/Enter in form controls The delegated keydown listener on #plugins-panel was calling preventDefault() unconditionally for Enter and Space before checking whether the target was actually an actionable element (badge, button). This blocked typing spaces in the search box and pressing Enter on select dropdowns. Fix: dispatchPluginAction() now returns a boolean indicating whether it matched an action. The keydown handler only calls preventDefault() when an action was actually dispatched. Add regression tests: Space in #plugin-search and Enter on #plugin-mode-filter must not have defaultPrevented set. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: Gabriel Costa <gabrielcg@proton.me> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent b9440d7 commit 1b2e91f

File tree

3 files changed

+602
-23
lines changed

3 files changed

+602
-23
lines changed

mcpgateway/static/admin.js

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8718,6 +8718,7 @@ function showTab(tabName) {
87188718
{
87198719
method: "GET",
87208720
credentials: "same-origin",
8721+
cache: "no-store",
87218722
headers: {
87228723
Accept: "text/html",
87238724
},
@@ -25236,9 +25237,9 @@ function initializePluginFunctions() {
2523625237
function updateBadgeHighlighting(type, value) {
2523725238
// Define selectors for each type
2523825239
const selectors = {
25239-
hook: "[onclick^='filterByHook']",
25240-
tag: "[onclick^='filterByTag']",
25241-
author: "[onclick^='filterByAuthor']",
25240+
hook: "[data-filter-hook]",
25241+
tag: "[data-filter-tag]",
25242+
author: "[data-filter-author]",
2524225243
};
2524325244

2524425245
const selector = selectors[type];
@@ -25251,12 +25252,16 @@ function initializePluginFunctions() {
2525125252

2525225253
badges.forEach((badge) => {
2525325254
// Check if this is the "All" badge (empty value)
25254-
const isAllBadge = badge.getAttribute("onclick").includes("('')");
25255+
const isAllBadge =
25256+
badge.dataset.filterHook === "" ||
25257+
badge.dataset.filterTag === "" ||
25258+
badge.dataset.filterAuthor === "";
2525525259

2525625260
// Check if this badge matches the selected value
25257-
const badgeValue = badge
25258-
.getAttribute("onclick")
25259-
.match(/'([^']*)'/)?.[1];
25261+
const badgeValue =
25262+
badge.dataset.filterHook ??
25263+
badge.dataset.filterTag ??
25264+
badge.dataset.filterAuthor;
2526025265
const isSelected =
2526125266
value === ""
2526225267
? isAllBadge
@@ -25444,6 +25449,44 @@ function initializePluginFunctions() {
2544425449
modal.classList.add("hidden");
2544525450
}
2544625451
};
25452+
25453+
// Single listener on the filter section — catches input/change from all child controls
25454+
const filtersSection = document.getElementById("plugin-filters");
25455+
if (filtersSection) {
25456+
filtersSection.addEventListener("input", () => window.filterPlugins());
25457+
filtersSection.addEventListener("change", () => window.filterPlugins());
25458+
}
25459+
25460+
// Single delegated click/keydown listener for badges, View Details, and modal close.
25461+
// Returns true if an action was dispatched, false otherwise.
25462+
function dispatchPluginAction(target) {
25463+
const hookEl = target.closest("[data-filter-hook]");
25464+
const tagEl = target.closest("[data-filter-tag]");
25465+
const authorEl = target.closest("[data-filter-author]");
25466+
const detailEl = target.closest("[data-show-plugin]");
25467+
const closeEl = target.closest("[data-close-plugin-modal]");
25468+
25469+
if (hookEl) window.filterByHook(hookEl.dataset.filterHook);
25470+
else if (tagEl) window.filterByTag(tagEl.dataset.filterTag);
25471+
else if (authorEl) window.filterByAuthor(authorEl.dataset.filterAuthor);
25472+
else if (detailEl) {
25473+
window.showPluginDetails(detailEl.dataset.showPlugin);
25474+
} else if (closeEl) window.closePluginDetails();
25475+
else return false;
25476+
return true;
25477+
}
25478+
25479+
const pluginsPanel = document.getElementById("plugins-panel");
25480+
if (pluginsPanel) {
25481+
pluginsPanel.addEventListener("click", (e) =>
25482+
dispatchPluginAction(e.target),
25483+
);
25484+
pluginsPanel.addEventListener("keydown", (e) => {
25485+
if (e.key !== "Enter" && e.key !== " ") return;
25486+
if (!dispatchPluginAction(e.target)) return;
25487+
e.preventDefault();
25488+
});
25489+
}
2544725490
}
2544825491

2544925492
// Initialize plugin functions if plugins panel exists

mcpgateway/templates/plugins_partial.html

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
8080
<div class="space-y-2">
8181
<div
8282
class="flex justify-between items-center p-2 bg-indigo-50 rounded dark:bg-indigo-900/20 cursor-pointer hover:bg-indigo-100 dark:hover:bg-indigo-900/30 transition-colors border border-indigo-200 dark:border-indigo-800"
83-
onclick="filterByHook('')" onkeydown="handleKeydown(event, () => filterByHook(''))" role="button" tabindex="0"
83+
data-filter-hook="" role="button" tabindex="0"
8484
>
8585
<span
8686
class="text-sm font-medium text-indigo-700 dark:text-indigo-300"
@@ -95,7 +95,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
9595
{% for hook, count in stats.plugins_by_hook.items() %}
9696
<div
9797
class="flex justify-between items-center p-2 bg-gray-50 rounded dark:bg-gray-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
98-
onclick="filterByHook({{ hook|tojson_attr }})" onkeydown="handleKeydown(event, () => filterByHook({{ hook|tojson_attr }}))" role="button" tabindex="0"
98+
data-filter-hook="{{ hook }}" role="button" tabindex="0"
9999
>
100100
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
101101
>{{ hook.replace('_', ' ').title() }}</span
@@ -131,7 +131,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
131131
<div class="flex flex-wrap gap-2">
132132
<span
133133
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200 cursor-pointer hover:bg-indigo-200 dark:hover:bg-indigo-800 transition-colors border border-indigo-300 dark:border-indigo-700"
134-
onclick="filterByTag('')" onkeydown="handleKeydown(event, () => filterByTag(''))" role="button" tabindex="0"
134+
data-filter-tag="" role="button" tabindex="0"
135135
>
136136
All Tags
137137
<span class="ml-1 text-xs text-indigo-600 dark:text-indigo-300"
@@ -141,7 +141,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
141141
{% for tag, count in stats.plugins_by_tag.items() %}
142142
<span
143143
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
144-
onclick="filterByTag({{ tag|tojson_attr }})" onkeydown="handleKeydown(event, () => filterByTag({{ tag|tojson_attr }}))" role="button" tabindex="0"
144+
data-filter-tag="{{ tag }}" role="button" tabindex="0"
145145
>
146146
{{ tag }}
147147
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400"
@@ -173,7 +173,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
173173
<div class="flex flex-wrap gap-2">
174174
<span
175175
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200 cursor-pointer hover:bg-indigo-200 dark:hover:bg-indigo-800 transition-colors border border-indigo-300 dark:border-indigo-700"
176-
onclick="filterByAuthor('')" onkeydown="handleKeydown(event, () => filterByAuthor(''))" role="button" tabindex="0"
176+
data-filter-author="" role="button" tabindex="0"
177177
>
178178
All Authors
179179
<span class="ml-1 text-xs text-indigo-600 dark:text-indigo-300"
@@ -183,7 +183,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
183183
{% for author, count in stats.plugins_by_author.items() %}
184184
<span
185185
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
186-
onclick="filterByAuthor({{ author|tojson_attr }})" onkeydown="handleKeydown(event, () => filterByAuthor({{ author|tojson_attr }}))" role="button" tabindex="0"
186+
data-filter-author="{{ author }}" role="button" tabindex="0"
187187
>
188188
{{ author }}
189189
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400"
@@ -196,21 +196,19 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
196196
</div>
197197

198198
<!-- Search and Filter Controls -->
199-
<div class="bg-white rounded-lg shadow p-4 dark:bg-gray-800">
199+
<div id="plugin-filters" class="bg-white rounded-lg shadow p-4 dark:bg-gray-800">
200200
<div class="flex flex-col md:flex-row gap-4">
201201
<div class="flex-1">
202202
<input
203203
type="text"
204204
id="plugin-search"
205205
placeholder="Search plugins by name, description, or author..."
206206
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
207-
oninput="filterPlugins()"
208207
/>
209208
</div>
210209
<div class="flex gap-2">
211210
<select
212211
id="plugin-mode-filter"
213-
onchange="filterPlugins()"
214212
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
215213
>
216214
<option value="">All Modes</option>
@@ -220,7 +218,6 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
220218
</select>
221219
<select
222220
id="plugin-status-filter"
223-
onchange="filterPlugins()"
224221
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
225222
>
226223
<option value="">All Status</option>
@@ -229,21 +226,18 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
229226
</select>
230227
<select
231228
id="plugin-hook-filter"
232-
onchange="filterPlugins()"
233229
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
234230
>
235231
<option value="">All Hooks</option>
236232
</select>
237233
<select
238234
id="plugin-tag-filter"
239-
onchange="filterPlugins()"
240235
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
241236
>
242237
<option value="">All Tags</option>
243238
</select>
244239
<select
245240
id="plugin-author-filter"
246-
onchange="filterPlugins()"
247241
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
248242
>
249243
<option value="">All Authors</option>
@@ -263,7 +257,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
263257
data-name="{{ plugin.name | lower }}"
264258
data-description="{{ plugin.description | decode_html | lower }}"
265259
data-author="{{ plugin.author | lower }}"
266-
data-mode="{{ plugin.mode }}"
260+
data-mode="{{ plugin.mode | lower }}"
267261
data-status="{{ plugin.status }}"
268262
data-hooks="{{ plugin.hooks | join(',') if plugin.hooks else '' }}"
269263
data-tags="{{ plugin.tags | join(',') if plugin.tags else '' }}"
@@ -378,7 +372,7 @@ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
378372

379373
<!-- Action Button -->
380374
<button
381-
onclick="showPluginDetails({{ plugin.name|tojson_attr }})"
375+
data-show-plugin="{{ plugin.name }}"
382376
class="w-full mt-4 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors"
383377
>
384378
View Details →
@@ -429,7 +423,7 @@ <h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
429423
Plugin Details
430424
</h3>
431425
<button
432-
onclick="closePluginDetails()"
426+
data-close-plugin-modal
433427
class="text-gray-400 hover:text-gray-500"
434428
>
435429
<svg

0 commit comments

Comments
 (0)