Move DOMPurify hooks to chats module
| @@ -263,7 +263,7 @@ import { | ||
| 263 | 263 | loadTabbyModels, |
| 264 | 264 | loadGenericModels, |
| 265 | 265 | } from './scripts/textgen-models.js'; |
| 266 | 266 | import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js'; |
| 267 | 267 | import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; |
| 268 | 268 | import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; |
| 269 | 269 | import { currentUser, setUserControls } from './scripts/user.js'; |
| @@ -363,130 +363,6 @@ toastr.options = { | ||
| 363 | 363 | }, |
| 364 | 364 | }; |
| 365 | 365 | |
| 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 | - | |
| 490 | 366 | // Event source init |
| 491 | 367 | //MARK: event_types |
| 492 | 368 | export const event_types = { |
| @@ -993,6 +869,7 @@ async function firstLoadInit() { | ||
| 993 | 869 | initStandaloneMode(); |
| 994 | 870 | initLibraryShims(); |
| 995 | 871 | addShowdownPatch(showdown); |
| 872 | + addDOMPurifyHooks(); | |
| 996 | 873 | reloadMarkdownProcessor(); |
| 997 | 874 | applyBrowserFixes(); |
| 998 | 875 | await getClientVersion(); |
| @@ -1716,6 +1716,132 @@ export function registerFileConverter(mimeType, converter) { | ||
| 1716 | 1716 | converters[mimeType] = converter; |
| 1717 | 1717 | } |
| 1718 | 1718 | |
| 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 | + | |
| 1719 | 1845 | export function initChatUtilities() { |
| 1720 | 1846 | $(document).on('click', '.mes_hide', async function () { |
| 1721 | 1847 | const messageBlock = $(this).closest('.mes'); |