Move DOMPurify hooks to chats module

3d488a5e7bc1b0951060a28a93262c503eae5bea

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +128 -125Showing whitespace changes
public/script.js+2 -125
@@ -263,7 +263,7 @@ import {
263263 loadTabbyModels,
264264 loadGenericModels,
265265} from './scripts/textgen-models.js';
266266import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
267267import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
268268import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
269269import { currentUser, setUserControls } from './scripts/user.js';
@@ -363,130 +363,6 @@ toastr.options = {
363363 },
364364};
365365
366-// Allow target="_blank" in links
367-DOMPurify.addHook('afterSanitizeAttributes', function (node) {
368- if ('target' in node) {
369- node.setAttribute('target', '_blank');
370- node.setAttribute('rel', 'noopener');
371- }
372-});
373-
374-DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
375- if (!config['MESSAGE_SANITIZE']) {
376- return;
377- }
378-
379- /* Retain the classes on UI elements of messages that interact with the main UI */
380- const permittedNodeTypes = ['BUTTON', 'DIV'];
381- if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
382- return;
383- }
384-
385- switch (data.attrName) {
386- case 'class': {
387- if (data.attrValue) {
388- data.attrValue = data.attrValue.split(' ').map((v) => {
389- if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') {
390- return v;
391- }
392-
393- return 'custom-' + v;
394- }).join(' ');
395- }
396- break;
397- }
398- }
399-});
400-
401-DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
402- if (!config['MESSAGE_SANITIZE']) {
403- return;
404- }
405-
406- // Replace line breaks with <br> in unknown elements
407- if (node instanceof HTMLUnknownElement) {
408- node.innerHTML = node.innerHTML.trim().replaceAll('\n', '<br>');
409- }
410-
411- const isMediaAllowed = isExternalMediaAllowed();
412- if (isMediaAllowed) {
413- return;
414- }
415-
416- if (!(node instanceof Element)) {
417- return;
418- }
419-
420- let mediaBlocked = false;
421-
422- switch (node.tagName) {
423- case 'AUDIO':
424- case 'VIDEO':
425- case 'SOURCE':
426- case 'TRACK':
427- case 'EMBED':
428- case 'OBJECT':
429- case 'IMG': {
430- const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
431- const src = node.getAttribute('src');
432- const data = node.getAttribute('data');
433- const srcset = node.getAttribute('srcset');
434-
435- if (srcset) {
436- const srcsetUrls = srcset.split(',');
437-
438- for (const srcsetUrl of srcsetUrls) {
439- const [url] = srcsetUrl.trim().split(' ');
440-
441- if (isExternalUrl(url)) {
442- console.warn('External media blocked', url);
443- node.remove();
444- mediaBlocked = true;
445- break;
446- }
447- }
448- }
449-
450- if (src && isExternalUrl(src)) {
451- console.warn('External media blocked', src);
452- mediaBlocked = true;
453- node.remove();
454- }
455-
456- if (data && isExternalUrl(data)) {
457- console.warn('External media blocked', data);
458- mediaBlocked = true;
459- node.remove();
460- }
461-
462- if (mediaBlocked && (node instanceof HTMLMediaElement)) {
463- node.autoplay = false;
464- node.pause();
465- }
466- }
467- break;
468- }
469-
470- if (mediaBlocked) {
471- const entityId = getCurrentEntityId();
472- const warningShownKey = `mediaWarningShown:${entityId}`;
473-
474- if (accountStorage.getItem(warningShownKey) === null) {
475- const warningToast = toastr.warning(
476- t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
477- t`External media has been blocked`,
478- {
479- timeOut: 0,
480- preventDuplicates: true,
481- onclick: () => toastr.clear(warningToast),
482- },
483- );
484-
485- accountStorage.setItem(warningShownKey, 'true');
486- }
487- }
488-});
489-
490366// Event source init
491367//MARK: event_types
492368export const event_types = {
@@ -993,6 +869,7 @@ async function firstLoadInit() {
993869 initStandaloneMode();
994870 initLibraryShims();
995871 addShowdownPatch(showdown);
872+ addDOMPurifyHooks();
996873 reloadMarkdownProcessor();
997874 applyBrowserFixes();
998875 await getClientVersion();
public/scripts/chats.js+126 -0
@@ -1716,6 +1716,132 @@ export function registerFileConverter(mimeType, converter) {
17161716 converters[mimeType] = converter;
17171717}
17181718
1719+export function addDOMPurifyHooks() {
1720+ // Allow target="_blank" in links
1721+ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
1722+ if ('target' in node) {
1723+ node.setAttribute('target', '_blank');
1724+ node.setAttribute('rel', 'noopener');
1725+ }
1726+ });
1727+
1728+ DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
1729+ if (!config['MESSAGE_SANITIZE']) {
1730+ return;
1731+ }
1732+
1733+ /* Retain the classes on UI elements of messages that interact with the main UI */
1734+ const permittedNodeTypes = ['BUTTON', 'DIV'];
1735+ if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
1736+ return;
1737+ }
1738+
1739+ switch (data.attrName) {
1740+ case 'class': {
1741+ if (data.attrValue) {
1742+ data.attrValue = data.attrValue.split(' ').map((v) => {
1743+ if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') {
1744+ return v;
1745+ }
1746+
1747+ return 'custom-' + v;
1748+ }).join(' ');
1749+ }
1750+ break;
1751+ }
1752+ }
1753+ });
1754+
1755+ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
1756+ if (!config['MESSAGE_SANITIZE']) {
1757+ return;
1758+ }
1759+
1760+ // Replace line breaks with <br> in unknown elements
1761+ if (node instanceof HTMLUnknownElement) {
1762+ node.innerHTML = node.innerHTML.trim().replaceAll('\n', '<br>');
1763+ }
1764+
1765+ const isMediaAllowed = isExternalMediaAllowed();
1766+ if (isMediaAllowed) {
1767+ return;
1768+ }
1769+
1770+ if (!(node instanceof Element)) {
1771+ return;
1772+ }
1773+
1774+ let mediaBlocked = false;
1775+
1776+ switch (node.tagName) {
1777+ case 'AUDIO':
1778+ case 'VIDEO':
1779+ case 'SOURCE':
1780+ case 'TRACK':
1781+ case 'EMBED':
1782+ case 'OBJECT':
1783+ case 'IMG': {
1784+ const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
1785+ const src = node.getAttribute('src');
1786+ const data = node.getAttribute('data');
1787+ const srcset = node.getAttribute('srcset');
1788+
1789+ if (srcset) {
1790+ const srcsetUrls = srcset.split(',');
1791+
1792+ for (const srcsetUrl of srcsetUrls) {
1793+ const [url] = srcsetUrl.trim().split(' ');
1794+
1795+ if (isExternalUrl(url)) {
1796+ console.warn('External media blocked', url);
1797+ node.remove();
1798+ mediaBlocked = true;
1799+ break;
1800+ }
1801+ }
1802+ }
1803+
1804+ if (src && isExternalUrl(src)) {
1805+ console.warn('External media blocked', src);
1806+ mediaBlocked = true;
1807+ node.remove();
1808+ }
1809+
1810+ if (data && isExternalUrl(data)) {
1811+ console.warn('External media blocked', data);
1812+ mediaBlocked = true;
1813+ node.remove();
1814+ }
1815+
1816+ if (mediaBlocked && (node instanceof HTMLMediaElement)) {
1817+ node.autoplay = false;
1818+ node.pause();
1819+ }
1820+ }
1821+ break;
1822+ }
1823+
1824+ if (mediaBlocked) {
1825+ const entityId = getCurrentEntityId();
1826+ const warningShownKey = `mediaWarningShown:${entityId}`;
1827+
1828+ if (accountStorage.getItem(warningShownKey) === null) {
1829+ const warningToast = toastr.warning(
1830+ t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
1831+ t`External media has been blocked`,
1832+ {
1833+ timeOut: 0,
1834+ preventDuplicates: true,
1835+ onclick: () => toastr.clear(warningToast),
1836+ },
1837+ );
1838+
1839+ accountStorage.setItem(warningShownKey, 'true');
1840+ }
1841+ }
1842+ });
1843+}
1844+
17191845export function initChatUtilities() {
17201846 $(document).on('click', '.mes_hide', async function () {
17211847 const messageBlock = $(this).closest('.mes');