mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-23 19:03:18 +00:00
The scheme and origin checks lived in a boolean guard in another function, so the raw query parameter still reached the iframe src, the history entry and window.open. The validator now returns the normalised href or null, and every sink consumes only that. Which URLs are accepted does not change: openIframe still checks the scheme alone, because the modal opens configured targets that are not among the page's iframe links. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
5.4 KiB
JavaScript
165 lines
5.4 KiB
JavaScript
const i18nBlock = document.getElementById('i18n');
|
|
window.I18N = i18nBlock ? JSON.parse(i18nBlock.textContent) : {};
|
|
|
|
function t(source) {
|
|
return (window.I18N || {})[source] || source;
|
|
}
|
|
|
|
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
|
|
|
|
function safeUrl(url) {
|
|
try {
|
|
const parsed = new URL(String(url == null ? '' : url), window.location.href);
|
|
return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isSafeUrl(url) {
|
|
return safeUrl(url) !== null;
|
|
}
|
|
|
|
function iconAndName(item) {
|
|
const nodes = [];
|
|
if (item.icon && item.icon.class) {
|
|
const icon = document.createElement('i');
|
|
icon.className = item.icon.class;
|
|
nodes.push(icon, document.createTextNode(' '));
|
|
}
|
|
nodes.push(document.createTextNode(item.name == null ? '' : item.name));
|
|
return nodes;
|
|
}
|
|
|
|
function renderMarkdown(content) {
|
|
const escaped = String(content).replace(/</g, '<').replace(/>/g, '>');
|
|
const parsed = new DOMParser().parseFromString(marked.parse(escaped), 'text/html');
|
|
|
|
parsed.querySelectorAll('a[href]').forEach((anchor) => {
|
|
if (!SAFE_URL_SCHEMES.includes(anchor.protocol)) {
|
|
anchor.replaceWith(...anchor.childNodes);
|
|
}
|
|
});
|
|
parsed.querySelectorAll('img[src]').forEach((image) => {
|
|
if (!SAFE_URL_SCHEMES.includes(image.protocol)) {
|
|
image.replaceWith(image.alt || '');
|
|
}
|
|
});
|
|
|
|
return parsed.body.innerHTML;
|
|
}
|
|
|
|
function openDynamicPopup(subitem) {
|
|
closeAllModals();
|
|
const modalTitle = document.getElementById('dynamicModalLabel');
|
|
modalTitle.replaceChildren(...iconAndName(subitem));
|
|
|
|
const identifierBox = document.getElementById('dynamicIdentifierBox');
|
|
const modalContent = document.getElementById('dynamicModalContent');
|
|
if (subitem.identifier) {
|
|
identifierBox.classList.remove('d-none');
|
|
modalContent.value = subitem.identifier;
|
|
} else {
|
|
identifierBox.classList.add('d-none');
|
|
modalContent.value = '';
|
|
}
|
|
|
|
function toggleBox(boxId, textId, content) {
|
|
const box = document.getElementById(boxId);
|
|
if (content) {
|
|
box.classList.remove('d-none');
|
|
document.getElementById(textId).innerHTML = renderMarkdown(content);
|
|
} else {
|
|
box.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
toggleBox('dynamicModalWarning', 'dynamicModalWarningText', subitem.warning);
|
|
toggleBox('dynamicModalInfo', 'dynamicModalInfoText', subitem.info);
|
|
|
|
const descriptionText = document.getElementById('dynamicDescriptionText');
|
|
if (!subitem.url && subitem.description) {
|
|
descriptionText.classList.remove('d-none');
|
|
descriptionText.innerText = subitem.description;
|
|
} else {
|
|
descriptionText.classList.add('d-none');
|
|
descriptionText.innerText = '';
|
|
}
|
|
|
|
const linkBox = document.getElementById('dynamicModalLink');
|
|
const linkHref = document.getElementById('dynamicModalLinkHref');
|
|
if (subitem.url) {
|
|
linkBox.classList.remove('d-none');
|
|
linkHref.href = subitem.url;
|
|
if (!isSafeUrl(subitem.url)) {
|
|
linkHref.removeAttribute('href');
|
|
}
|
|
linkHref.innerText = subitem.description || t("Open Link");
|
|
linkHref.classList.remove('iframe');
|
|
linkHref.onclick = null;
|
|
if (subitem.iframe) {
|
|
linkHref.classList.add('iframe');
|
|
linkHref.onclick = function(event) {
|
|
event.preventDefault();
|
|
openIframe(subitem.url);
|
|
closeAllModals();
|
|
};
|
|
}
|
|
} else {
|
|
linkBox.classList.add('d-none');
|
|
linkHref.href = '#';
|
|
}
|
|
function populateSection(sectionId, listId, items, onClickHandler) {
|
|
const section = document.getElementById(sectionId);
|
|
const list = document.getElementById(listId);
|
|
list.innerHTML = '';
|
|
|
|
if (items && items.length > 0) {
|
|
section.classList.remove('d-none');
|
|
items.forEach(item => {
|
|
const listItem = document.createElement('li');
|
|
listItem.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'align-items-center');
|
|
const label = document.createElement('span');
|
|
label.replaceChildren(...iconAndName(item));
|
|
const button = document.createElement('button');
|
|
button.className = 'btn btn-outline-secondary btn-sm';
|
|
button.textContent = t('Open');
|
|
listItem.replaceChildren(label, button);
|
|
button.addEventListener('click', () => onClickHandler(item));
|
|
list.appendChild(listItem);
|
|
});
|
|
} else {
|
|
section.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
populateSection('dynamicAlternativesSection', 'dynamicAlternativesList', subitem.alternatives, openDynamicPopup);
|
|
populateSection('dynamicChildrenSection', 'dynamicChildrenList', subitem.children, openDynamicPopup);
|
|
|
|
const copyButton = document.getElementById('dynamicCopyButton');
|
|
copyButton.onclick = () => {
|
|
modalContent.select();
|
|
navigator.clipboard.writeText(modalContent.value).then(() => {
|
|
alert(t('Identifier copied to clipboard!'));
|
|
});
|
|
};
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('dynamicModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function closeAllModals() {
|
|
const modals = document.querySelectorAll('.modal.show');
|
|
modals.forEach(modal => {
|
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
}
|
|
});
|
|
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
backdrops.forEach(backdrop => backdrop.remove());
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.overflow = '';
|
|
document.body.style.paddingRight = '';
|
|
}
|