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 {
263 loadTabbyModels,263 loadTabbyModels,
264 loadGenericModels,264 loadGenericModels,
265} from './scripts/textgen-models.js';265} from './scripts/textgen-models.js';
266import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';266import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
267import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';267import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
268import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';268import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
269import { currentUser, setUserControls } from './scripts/user.js';269import { currentUser, setUserControls } from './scripts/user.js';
@@ -363,130 +363,6 @@ toastr.options = {
363 },363 },
364};364};
365365
366// Allow target="_blank" in links
367DOMPurify.addHook('afterSanitizeAttributes', function (node) {
368 if ('target' in node) {
369 node.setAttribute('target', '_blank');
370 node.setAttribute('rel', 'noopener');
371 }
372});
373
374DOMPurify.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
401DOMPurify.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
490// Event source init366// Event source init
491//MARK: event_types367//MARK: event_types
492export const event_types = {368export const event_types = {
@@ -993,6 +869,7 @@ async function firstLoadInit() {
993 initStandaloneMode();869 initStandaloneMode();
994 initLibraryShims();870 initLibraryShims();
995 addShowdownPatch(showdown);871 addShowdownPatch(showdown);
872 addDOMPurifyHooks();
996 reloadMarkdownProcessor();873 reloadMarkdownProcessor();
997 applyBrowserFixes();874 applyBrowserFixes();
998 await getClientVersion();875 await getClientVersion();
public/scripts/chats.js+126 -0
@@ -1716,6 +1716,132 @@ export function registerFileConverter(mimeType, converter) {
1716 converters[mimeType] = converter;1716 converters[mimeType] = converter;
1717}1717}
17181718
1719export 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
1719export function initChatUtilities() {1845export function initChatUtilities() {
1720 $(document).on('click', '.mes_hide', async function () {1846 $(document).on('click', '.mes_hide', async function () {
1721 const messageBlock = $(this).closest('.mes');1847 const messageBlock = $(this).closest('.mes');