mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-08-24 13:14:32 +00:00
Every one of these paths checked a string that the browser reinterprets afterwards. isSafeUrl now hangs the value on an <a> and reads back probe.protocol, so the check sees what the browser will see: a pre-parse test reads "javascript:" as a relative path and passes it, and the HTML parser then decodes it to "javascript:". marked passes raw HTML through and emits hrefs unescaped. renderMarkdown escapes the angle brackets before parsing, parses into an inert DOMParser document where no script runs and no image loads, and drops anchors and images whose scheme is not http, https or mailto. Blockquotes and <autolinks> stop working as a result; neither appears in the configuration. modalTitle and the alternatives list interpolated subitem.name and icon.class into innerHTML. Both are built as nodes now. name is a translatable key, so it arrives from the machine-written catalogues. The link kept its click handler and its class across popups, because one anchor serves all of them: a later, unrelated click opened whatever an earlier popup pointed at, and addEventListener stacked one handler per open. Both are reset per popup and the handler is assigned, not added. openIframe guards its own argument. Removing the href alone left the handler passing the raw URL on, and ?iframe= in the query string reaches the same sink with no configuration involved at all. Verified in headless Chromium: decimal and hex character references, 	- and 
-split schemes, reference-style links, raw HTML as a link's text, and the two name sinks all executed before these changes. injection.spec.js keeps all fifteen payloads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
194 lines
6.4 KiB
JavaScript
194 lines
6.4 KiB
JavaScript
// Global variables to store elements and original state
|
||
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
|
||
let currentIframeUrl = null;
|
||
|
||
// === Auto-open iframe if URL parameter is present ===
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
const paramUrl = new URLSearchParams(window.location.search).get('iframe');
|
||
if (paramUrl) {
|
||
currentIframeUrl = paramUrl;
|
||
enterFullscreen();
|
||
openIframe(paramUrl);
|
||
}
|
||
});
|
||
|
||
// Synchronize the height of the iframe to match the scroll-container or main element
|
||
function syncIframeHeight() {
|
||
const iframe = mainElement.querySelector("iframe");
|
||
if (iframe) {
|
||
if (scrollbarContainer) {
|
||
// Prefer inline height, otherwise inline max-height
|
||
const inlineHeight = scrollbarContainer.style.height;
|
||
const inlineMax = scrollbarContainer.style.maxHeight;
|
||
const target = inlineHeight || inlineMax;
|
||
if (target) {
|
||
iframe.style.height = target;
|
||
} else {
|
||
iframe.style.height = mainElement.style.height;
|
||
}
|
||
} else {
|
||
iframe.style.height = mainElement.style.height;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Function to open a URL in an iframe (jQuery version mit 1500 ms Fade)
|
||
function openIframe(url) {
|
||
if (!isSafeUrl(url)) {
|
||
return;
|
||
}
|
||
|
||
var $container = scrollbarContainer ? $(scrollbarContainer) : null;
|
||
var $customScroll = customScrollbar ? $(customScrollbar) : null;
|
||
var $main = $(mainElement);
|
||
|
||
// Original-Content ausblenden mit 1500 ms
|
||
var promises = [];
|
||
if ($container) promises.push($container.fadeOut(1500).promise());
|
||
if ($customScroll) promises.push($customScroll.fadeOut(1500).promise());
|
||
|
||
$.when.apply($, promises).done(function() {
|
||
// now that scroll areas are hidden, go fullscreen
|
||
enterFullscreen();
|
||
// create iframe if it doesn’t exist yet
|
||
var $iframe = $main.find('iframe');
|
||
if ($iframe.length === 0) {
|
||
originalMainStyle = $main.attr('style') || null;
|
||
$iframe = $('<iframe>', {
|
||
width: '100%',
|
||
frameborder: 0,
|
||
scrolling: 'auto'
|
||
}).css({ overflow: 'auto', display: 'none' });
|
||
$main.append($iframe);
|
||
}
|
||
|
||
// Quelle setzen und mit 1500 ms einblenden
|
||
$iframe
|
||
.attr('src', url)
|
||
.fadeIn(1500, function() {
|
||
syncIframeHeight();
|
||
observeIframeNavigation();
|
||
});
|
||
|
||
// URL-State pushen
|
||
var newUrl = new URL(window.location);
|
||
newUrl.searchParams.set('iframe', url);
|
||
window.history.pushState({ iframe: url }, '', newUrl);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Restore the original <main> content and exit fullscreen.
|
||
*/
|
||
function restoreOriginal() {
|
||
// Exit fullscreen (collapse header/footer and run recalcs)
|
||
exitFullscreen();
|
||
|
||
// Replace <main> innerHTML with the snapshot we took on load
|
||
mainElement.innerHTML = originalContent;
|
||
|
||
// Reset any inline styles on mainElement
|
||
if (originalMainStyle !== null) {
|
||
mainElement.setAttribute('style', originalMainStyle);
|
||
} else {
|
||
mainElement.removeAttribute('style');
|
||
}
|
||
|
||
// Re-run height adjustments for scroll container & thumb
|
||
adjustScrollContainerHeight();
|
||
updateCustomScrollbar();
|
||
|
||
// Clear iframe state and URL param
|
||
currentIframeUrl = null;
|
||
history.replaceState(null, '', window.location.pathname);
|
||
}
|
||
|
||
// Initialize event listeners after DOM content is loaded
|
||
document.addEventListener("DOMContentLoaded", function() {
|
||
// Cache references to elements and original state
|
||
mainElement = document.querySelector("main");
|
||
originalContent = mainElement.innerHTML;
|
||
originalMainStyle = mainElement.getAttribute("style"); // may be null
|
||
container = document.querySelector(".container");
|
||
customScrollbar = document.getElementById("custom-scrollbar");
|
||
scrollbarContainer = container.querySelector(".scroll-container")
|
||
|
||
document.querySelectorAll(".js-restore").forEach(el => {
|
||
el.style.cursor = "pointer";
|
||
el.addEventListener("click", restoreOriginal);
|
||
});
|
||
|
||
// === Close iframe & exit fullscreen when any .js-restore is clicked ===
|
||
document.querySelectorAll('.js-restore').forEach(el => {
|
||
el.style.cursor = 'pointer';
|
||
el.addEventListener('click', () => {
|
||
// first collapse header/footer and recalc container
|
||
exitFullscreen();
|
||
// then fade out and remove the iframe, fade content back
|
||
restoreOriginal();
|
||
// clear stored URL and reset browser address
|
||
currentIframeUrl = null;
|
||
history.replaceState(null, '', window.location.pathname);
|
||
});
|
||
});
|
||
|
||
});
|
||
|
||
/**
|
||
* Opens the current iframe URL in a new browser tab.
|
||
*/
|
||
function openIframeInNewTab() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const iframeUrl = params.get('iframe');
|
||
if (iframeUrl) {
|
||
window.open(iframeUrl, '_blank');
|
||
} else {
|
||
alert('No iframe is currently open.');
|
||
}
|
||
}
|
||
// expose globally so your template’s onclick can find it
|
||
window.openIframeInNewTab = openIframeInNewTab;
|
||
|
||
// Adjust iframe height on window resize
|
||
window.addEventListener('resize', syncIframeHeight);
|
||
|
||
/**
|
||
* Observe iframe location changes (Same-Origin only).
|
||
*/
|
||
function observeIframeNavigation() {
|
||
const iframe = mainElement.querySelector("iframe");
|
||
if (!iframe || !iframe.contentWindow) return;
|
||
|
||
let lastUrl = iframe.contentWindow.location.href;
|
||
|
||
setInterval(() => {
|
||
try {
|
||
const currentUrl = iframe.contentWindow.location.href;
|
||
if (currentUrl !== lastUrl) {
|
||
lastUrl = currentUrl;
|
||
const newUrl = new URL(window.location);
|
||
newUrl.searchParams.set('iframe', currentUrl);
|
||
window.history.replaceState({}, '', newUrl);
|
||
}
|
||
} catch (e) {
|
||
// Cross-origin – ignore
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
// Remember, open iframe, enter fullscreen, AND set the URL param immediately
|
||
document.querySelectorAll(".iframe-link").forEach(link => {
|
||
link.addEventListener("click", function(event) {
|
||
event.preventDefault();
|
||
currentIframeUrl = this.href;
|
||
|
||
enterFullscreen();
|
||
openIframe(currentIframeUrl);
|
||
|
||
// Update the browser URL right away
|
||
const newUrl = new URL(window.location);
|
||
newUrl.searchParams.set('iframe', currentIframeUrl);
|
||
window.history.replaceState({ iframe: currentIframeUrl }, '', newUrl);
|
||
});
|
||
});
|