Ensure unique selectors for loaded extension files
| @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque | |||
| 4 | import { showLoader } from './loader.js'; | 4 | import { showLoader } from './loader.js'; |
| 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | import { delay, isSubsetOf, setValueByPath } from './utils.js'; | 7 | import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js'; |
| 8 | import { getContext } from './st-context.js'; | 8 | import { getContext } from './st-context.js'; |
| 9 | import { isAdmin } from './user.js'; | 9 | import { isAdmin } from './user.js'; |
| 10 | import { t } from './i18n.js'; | 10 | import { t } from './i18n.js'; |
| @@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) { | |||
| 509 | 509 | ||
| 510 | return new Promise((resolve, reject) => { | 510 | return new Promise((resolve, reject) => { |
| 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; | 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; |
| 512 | const id = sanitizeSelector(`${name}-css`); | ||
| 512 | 513 | ||
| 513 | if ($(`link[id="${name}"]`).length === 0) { | 514 | if ($(`link[id="${id}"]`).length === 0) { |
| 514 | const link = document.createElement('link'); | 515 | const link = document.createElement('link'); |
| 515 | link.id = name; | 516 | link.id = id; |
| 516 | link.rel = 'stylesheet'; | 517 | link.rel = 'stylesheet'; |
| 517 | link.type = 'text/css'; | 518 | link.type = 'text/css'; |
| 518 | link.href = url; | 519 | link.href = url; |
| @@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) { | |||
| 540 | 541 | ||
| 541 | return new Promise((resolve, reject) => { | 542 | return new Promise((resolve, reject) => { |
| 542 | const url = `/scripts/extensions/${name}/${manifest.js}`; | 543 | const url = `/scripts/extensions/${name}/${manifest.js}`; |
| 544 | const id = sanitizeSelector(`${name}-js`); | ||
| 543 | let ready = false; | 545 | let ready = false; |
| 544 | 546 | ||
| 545 | if ($(`script[id="${name}"]`).length === 0) { | 547 | if ($(`script[id="${id}"]`).length === 0) { |
| 546 | const script = document.createElement('script'); | 548 | const script = document.createElement('script'); |
| 547 | script.id = name; | 549 | script.id = id; |
| 548 | script.type = 'module'; | 550 | script.type = 'module'; |
| 549 | script.src = url; | 551 | script.src = url; |
| 550 | script.async = true; | 552 | script.async = true; |
| @@ -67,6 +67,16 @@ export function escapeHtml(str) { | |||
| 67 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | 67 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | /** | ||
| 71 | * Make string safe for use as a CSS selector. | ||
| 72 | * @param {string} str String to sanitize | ||
| 73 | * @param {string} replacement Replacement for invalid characters | ||
| 74 | * @returns {string} Sanitized string | ||
| 75 | */ | ||
| 76 | export function sanitizeSelector(str, replacement = '_') { | ||
| 77 | return String(str).replace(/[^a-z0-9_-]/ig, replacement); | ||
| 78 | } | ||
| 79 | |||
| 70 | export function isValidUrl(value) { | 80 | export function isValidUrl(value) { |
| 71 | try { | 81 | try { |
| 72 | new URL(value); | 82 | new URL(value); |