mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
fix(web): restore htmx script re-execution timing that Alpine x-data depends on
Removing the custom htmx:afterSwap script-reexecution handler (in a prior
commit, as a "duplicate execution" cleanup) broke every partial whose Alpine
x-data component function is defined by an inline <script> in that same
partial (e.g. wifi.html's wifiSetup()) - confirmed live via browser console:
"Alpine Expression Error: wifiSetup is not defined" on every field in the
WiFi tab.
Root cause: htmx's own native script execution runs during its "settle"
phase (~20ms after swap, per htmx's own defaultSettleDelay), but Alpine's
MutationObserver evaluates x-data on newly-inserted elements synchronously,
right as the swap lands - before settle. So the inline <script> defining
wifiSetup() was still un-run when Alpine tried to call it, and Alpine does
not retry a failed x-data evaluation later once the function does become
defined.
Fix: re-execute swapped <script> tags ourselves on htmx:afterSwap (which
fires synchronously, before settle, beating Alpine's observer), and disable
htmx's own native script re-execution (htmx.config.allowScriptTags = false)
so the same script doesn't also run a second time during settle - restoring
correct timing without reintroducing the original double-execution bug.
Also in this commit:
- fix XSS: unescaped repoUrl in a title attribute in renderSavedRepositories
- replace .includes('github.com') substring checks with real URL hostname
validation (CodeQL: incomplete URL substring sanitization)
This commit is contained in:
@@ -162,14 +162,35 @@
|
||||
console.warn('HTMX swap error:', event.detail);
|
||||
});
|
||||
|
||||
// Note: no custom htmx:afterSwap script re-execution here.
|
||||
// htmx's own default config (allowScriptTags: true, on by
|
||||
// default - confirmed in the vendored htmx.min.js) already
|
||||
// clones and re-inserts <script> tags in swapped content
|
||||
// to make the browser execute them, exactly like a real
|
||||
// page load. A duplicate handler here previously did the
|
||||
// same clone-and-reinsert a second time, executing every
|
||||
// inline <script> in every HTMX-loaded partial TWICE.
|
||||
// Execute <script> tags in swapped content ourselves, on
|
||||
// htmx:afterSwap (synchronous, right after the swap) rather
|
||||
// than relying on htmx's own script handling, which runs
|
||||
// during its later "settle" phase (~20ms after swap, per
|
||||
// htmx's defaultSettleDelay). Alpine's MutationObserver
|
||||
// processes newly-inserted x-data elements synchronously
|
||||
// as soon as the swap lands, which is BEFORE htmx's settle
|
||||
// phase - so any partial whose x-data component function
|
||||
// (e.g. wifiSetup()) is defined by an inline <script> in
|
||||
// that same partial would have that script still un-run
|
||||
// when Alpine evaluates x-data, permanently failing with
|
||||
// "wifiSetup is not defined" (Alpine does not retry).
|
||||
// Disable htmx's own native script re-execution so the
|
||||
// same script doesn't also run a second time via settle.
|
||||
if (typeof htmx !== 'undefined' && htmx.config) {
|
||||
htmx.config.allowScriptTags = false;
|
||||
}
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
const target = event.detail && event.detail.target;
|
||||
if (!target || !(target instanceof Element)) return;
|
||||
target.querySelectorAll('script').forEach(function(oldScript) {
|
||||
const newScript = document.createElement('script');
|
||||
for (const attr of oldScript.attributes) {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
newScript.textContent = oldScript.textContent;
|
||||
oldScript.replaceWith(newScript);
|
||||
});
|
||||
});
|
||||
|
||||
// Mark tab containers as loaded once their content settles, so switching
|
||||
// away and back doesn't re-fetch. Scoped to the "loadtab" trigger (tab
|
||||
|
||||
@@ -667,7 +667,7 @@ window.handleGitHubPluginInstall = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
if (!isGithubUrl(repoUrl)) {
|
||||
if (statusDiv) {
|
||||
statusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
|
||||
}
|
||||
@@ -4022,9 +4022,9 @@ function renderSavedRepositories(repositories) {
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas ${repoType === 'registry' ? 'fa-folder-open' : 'fa-code-branch'} text-gray-400 text-xs"></i>
|
||||
<span class="text-sm font-medium text-gray-900 truncate" title="${repoUrl}">${escapeHtml(repoName)}</span>
|
||||
<span class="text-sm font-medium text-gray-900 truncate" title="${escapeAttribute(repoUrl)}">${escapeHtml(repoName)}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 truncate" title="${repoUrl}">${escapeHtml(repoUrl)}</p>
|
||||
<p class="text-xs text-gray-500 truncate" title="${escapeAttribute(repoUrl)}">${escapeHtml(repoUrl)}</p>
|
||||
</div>
|
||||
<button onclick='if(window.removeSavedRepository){window.removeSavedRepository(${escapeJs(repoUrl)})}else{console.error("removeSavedRepository not available")}' class="ml-2 text-red-600 hover:text-red-800 text-xs px-2 py-1" title="Remove repository">
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -4107,7 +4107,7 @@ function attachInstallButtonHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
if (!isGithubUrl(repoUrl)) {
|
||||
if (pluginStatusDiv) {
|
||||
pluginStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
|
||||
}
|
||||
@@ -4280,7 +4280,7 @@ function setupGitHubInstallHandlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
if (!isGithubUrl(repoUrl)) {
|
||||
registryStatusDiv.innerHTML = '<span class="text-red-600"><i class="fas fa-exclamation-circle mr-1"></i>Please enter a valid GitHub URL</span>';
|
||||
return;
|
||||
}
|
||||
@@ -4336,7 +4336,7 @@ function setupGitHubInstallHandlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repoUrl.includes('github.com')) {
|
||||
if (!isGithubUrl(repoUrl)) {
|
||||
showError('Please enter a valid GitHub URL');
|
||||
return;
|
||||
}
|
||||
@@ -4480,6 +4480,19 @@ function showError(message) {
|
||||
}
|
||||
|
||||
|
||||
// Validate that a URL's actual host is github.com (not just a substring
|
||||
// match, which 'evil.com/github.com' or 'github.com.evil.com' would pass).
|
||||
// This is only a UX nicety pointing users at a valid URL - the server does
|
||||
// its own proper hostname validation before actually acting on the URL.
|
||||
function isGithubUrl(url) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname.toLowerCase();
|
||||
return hostname === 'github.com' || hostname === 'www.github.com';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user