@mulmoclaude/core 4.4.1 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -55,7 +55,10 @@ collection's view-mode selector automatically.
|
|
|
55
55
|
|
|
56
56
|
## The runtime contract — `window.__MC_VIEW`
|
|
57
57
|
|
|
58
|
-
The host injects a bootstrap
|
|
58
|
+
The host injects a bootstrap at the very start of your `<head>`, so it runs
|
|
59
|
+
**before any script in your `<head>` or `<body>`** — write your view normally
|
|
60
|
+
and `window.__MC_VIEW` is always there. (A `<script>` placed _before_ `<head>`
|
|
61
|
+
would run ahead of it and see no `__MC_VIEW`; there is no reason to write one.)
|
|
59
62
|
|
|
60
63
|
```js
|
|
61
64
|
window.__MC_VIEW = {
|
|
@@ -63,6 +66,8 @@ window.__MC_VIEW = {
|
|
|
63
66
|
token: "<scoped capability token>", // Authorization bearer
|
|
64
67
|
dataUrl: "http://localhost:3001/api/collections/annual-plan/view-data",
|
|
65
68
|
onChange: (cb) => unsubscribe, // live refresh — see "Staying live" below
|
|
69
|
+
searchQuery: "", // live text in the app's own search box — see "One search box"
|
|
70
|
+
onSearchQueryChange: (cb) => unsubscribe, // fires when the user types there
|
|
66
71
|
openItem: (id, mode) => void, // open a record in the host's panel — see "Opening a record"
|
|
67
72
|
startChat: (prompt, role) => void, // draft a new chat for the user — see "Starting a chat"
|
|
68
73
|
};
|
|
@@ -299,6 +304,69 @@ What you need to know:
|
|
|
299
304
|
- It returns an **unsubscribe** function; you rarely need it (the view is torn
|
|
300
305
|
down with the iframe), but it's there for fine-grained control.
|
|
301
306
|
|
|
307
|
+
### One search box — `searchQuery` / `onSearchQueryChange`
|
|
308
|
+
|
|
309
|
+
The app's **standard search box stays on screen while your view renders**, so
|
|
310
|
+
the user expects it to drive your view too. It does: the host relays what they
|
|
311
|
+
type into the sandboxed frame.
|
|
312
|
+
|
|
313
|
+
**Do not build a second search box.** Read the host's, and you get the
|
|
314
|
+
behaviour the user already assumes.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
const v = window.__MC_VIEW;
|
|
318
|
+
|
|
319
|
+
function render() {
|
|
320
|
+
const q = v.searchQuery.trim().toLowerCase();
|
|
321
|
+
const hits = q ? records.filter((r) => matches(r, q)) : records;
|
|
322
|
+
// …draw hits, highlight where each one matched…
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
v.onSearchQueryChange(render); // the user typed in the app's search box
|
|
326
|
+
v.onChange(reload); // the data changed (a separate wire — see above)
|
|
327
|
+
render();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
What you need to know:
|
|
331
|
+
|
|
332
|
+
- **`searchQuery` is always current.** It updates the instant the user types, so
|
|
333
|
+
reading it inside your own render (as above) is never a keystroke behind.
|
|
334
|
+
- **The callback is debounced** — it runs once typing pauses (~150 ms), not
|
|
335
|
+
once per keystroke — and receives the query as its argument
|
|
336
|
+
(`onSearchQueryChange((q) => …)`), so you can use either source.
|
|
337
|
+
- **An empty string means the box is empty** — show everything, don't show
|
|
338
|
+
nothing. It fires on clearing too.
|
|
339
|
+
- **It is scoped to this collection and this session.** Switching collections
|
|
340
|
+
clears the box; nothing is persisted.
|
|
341
|
+
- **Navigating your frame away ends it.** The query arrives on a private
|
|
342
|
+
channel bound to the document the host built, so a page your view navigates
|
|
343
|
+
to gets no further queries — deliberately, since the text is the user's own.
|
|
344
|
+
If that page asks the host for the channel, the host reinstalls your view
|
|
345
|
+
over it rather than handing it over. Open outbound links with
|
|
346
|
+
`target="_blank"` (see the sandbox rules) and your view keeps running.
|
|
347
|
+
- **Reloading your own view is fine.** `location.reload()` makes the host
|
|
348
|
+
reinstall the view and hand it a fresh channel, seeded with whatever is in
|
|
349
|
+
the search box — so the query you see after a reload is the current one, not
|
|
350
|
+
an empty string.
|
|
351
|
+
- **Replacing the document is allowed a few times, not endlessly.** Each
|
|
352
|
+
reinstall — after a reload or after another page claims the channel — costs
|
|
353
|
+
the host a rebuild, so it grants **at most 3** between one user-initiated
|
|
354
|
+
view change and the next (switching view or collection resets the budget).
|
|
355
|
+
Past that your view still renders and still reads its data; only the search
|
|
356
|
+
channel stays disconnected, and `searchQuery` stops updating. Use `onChange`
|
|
357
|
+
to refresh rather than reloading the frame, and you will never reach it.
|
|
358
|
+
- **One direction only.** You read the user's query; you cannot write the app's
|
|
359
|
+
search box from view code. If your view needs a filter the box can't express
|
|
360
|
+
(a date range, a facet), add that control yourself and combine it with
|
|
361
|
+
`searchQuery`.
|
|
362
|
+
- **No extra capability needed**, and it is **not** a data reload: it never
|
|
363
|
+
fires `onChange`, and `onChange` never fires this.
|
|
364
|
+
|
|
365
|
+
This is what makes a "search" view worth writing — the built-in box only
|
|
366
|
+
filters table rows, so a view that shows **which field matched**, with a
|
|
367
|
+
highlighted snippet, is a real upgrade the user drives from the box they were
|
|
368
|
+
already using.
|
|
369
|
+
|
|
302
370
|
### Opening a record — `openItem`
|
|
303
371
|
|
|
304
372
|
Your view owns the _layout_ (a grid, a chart, a board); it doesn't have to
|
|
@@ -88,7 +88,7 @@ function useClipboardCopy(resetMs = DEFAULT_RESET_MS) {
|
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
|
90
90
|
//#region ../../node_modules/dompurify/dist/purify.es.mjs
|
|
91
|
-
/*! @license DOMPurify 3.4.
|
|
91
|
+
/*! @license DOMPurify 3.4.14 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.14/LICENSE */
|
|
92
92
|
function _arrayLikeToArray(r, a) {
|
|
93
93
|
(null == a || a > r.length) && (a = r.length);
|
|
94
94
|
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
@@ -817,6 +817,7 @@ var svg = freeze([
|
|
|
817
817
|
"patterncontentunits",
|
|
818
818
|
"patterntransform",
|
|
819
819
|
"patternunits",
|
|
820
|
+
"pointer-events",
|
|
820
821
|
"points",
|
|
821
822
|
"preservealpha",
|
|
822
823
|
"preserveaspectratio",
|
|
@@ -871,6 +872,7 @@ var svg = freeze([
|
|
|
871
872
|
"u2",
|
|
872
873
|
"unicode",
|
|
873
874
|
"values",
|
|
875
|
+
"vector-effect",
|
|
874
876
|
"viewbox",
|
|
875
877
|
"visibility",
|
|
876
878
|
"version",
|
|
@@ -984,6 +986,24 @@ var NODE_TYPE = {
|
|
|
984
986
|
documentFragment: 11,
|
|
985
987
|
notation: 12
|
|
986
988
|
};
|
|
989
|
+
var LITERAL_TEXT_ELEMENT_NAMES = [
|
|
990
|
+
"style",
|
|
991
|
+
"script",
|
|
992
|
+
"xmp",
|
|
993
|
+
"iframe",
|
|
994
|
+
"noembed",
|
|
995
|
+
"noframes",
|
|
996
|
+
"plaintext",
|
|
997
|
+
"noscript"
|
|
998
|
+
];
|
|
999
|
+
var LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
|
|
1000
|
+
var LITERAL_TEXT_CLOSE = function() {
|
|
1001
|
+
const map = {};
|
|
1002
|
+
arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
|
|
1003
|
+
map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
|
|
1004
|
+
});
|
|
1005
|
+
return freeze(map);
|
|
1006
|
+
}();
|
|
987
1007
|
var getGlobal = function getGlobal() {
|
|
988
1008
|
return typeof window === "undefined" ? null : window;
|
|
989
1009
|
};
|
|
@@ -1043,10 +1063,25 @@ var _createHooksMap = function _createHooksMap() {
|
|
|
1043
1063
|
var _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
|
|
1044
1064
|
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
|
|
1045
1065
|
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Resolve an object-valued configuration option: a prototype-free clone
|
|
1068
|
+
* of cfg[key] when it is an own, truthy object property, else a fresh
|
|
1069
|
+
* fallback built by makeFallback (fresh on every parse, so a previous
|
|
1070
|
+
* parse can never leak state into the next one).
|
|
1071
|
+
*
|
|
1072
|
+
* @param cfg the cloned, prototype-free configuration object
|
|
1073
|
+
* @param key the configuration property to read
|
|
1074
|
+
* @param makeFallback builds the fallback value when the option is absent
|
|
1075
|
+
* @returns the resolved object
|
|
1076
|
+
*/
|
|
1077
|
+
var _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
|
|
1078
|
+
const value = objectHasOwnProperty(cfg, key) ? cfg[key] : void 0;
|
|
1079
|
+
return value && typeof value === "object" ? clone(value) : makeFallback();
|
|
1080
|
+
};
|
|
1046
1081
|
function createDOMPurify() {
|
|
1047
1082
|
let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
1048
1083
|
const DOMPurify = (root) => createDOMPurify(root);
|
|
1049
|
-
DOMPurify.version = "3.4.
|
|
1084
|
+
DOMPurify.version = "3.4.14";
|
|
1050
1085
|
DOMPurify.removed = [];
|
|
1051
1086
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
1052
1087
|
DOMPurify.isSupported = false;
|
|
@@ -1071,6 +1106,12 @@ function createDOMPurify() {
|
|
|
1071
1106
|
const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null;
|
|
1072
1107
|
const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null;
|
|
1073
1108
|
const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, "ownerDocument") : null;
|
|
1109
|
+
const _readNodeType = function _readNodeType(node) {
|
|
1110
|
+
return getNodeType ? getNodeType(node) : node.nodeType;
|
|
1111
|
+
};
|
|
1112
|
+
const _readNodeName = function _readNodeName(node) {
|
|
1113
|
+
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
1114
|
+
};
|
|
1074
1115
|
if (typeof HTMLTemplateElement === "function") {
|
|
1075
1116
|
const template = document.createElement("template");
|
|
1076
1117
|
if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
|
|
@@ -1328,9 +1369,9 @@ function createDOMPurify() {
|
|
|
1328
1369
|
IN_PLACE = cfg.IN_PLACE || false;
|
|
1329
1370
|
IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
|
|
1330
1371
|
NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
|
|
1331
|
-
MATHML_TEXT_INTEGRATION_POINTS =
|
|
1332
|
-
HTML_INTEGRATION_POINTS =
|
|
1333
|
-
const customElementHandling =
|
|
1372
|
+
MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, "MATHML_TEXT_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS));
|
|
1373
|
+
HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, "HTML_INTEGRATION_POINTS", () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS));
|
|
1374
|
+
const customElementHandling = _resolveObjectOption(cfg, "CUSTOM_ELEMENT_HANDLING", () => create(null));
|
|
1334
1375
|
CUSTOM_ELEMENT_HANDLING = create(null);
|
|
1335
1376
|
if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
|
|
1336
1377
|
if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck;
|
|
@@ -1377,11 +1418,6 @@ function createDOMPurify() {
|
|
|
1377
1418
|
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
1378
1419
|
}
|
|
1379
1420
|
}
|
|
1380
|
-
if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
|
|
1381
|
-
if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) {
|
|
1382
|
-
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
1383
|
-
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
|
|
1384
|
-
}
|
|
1385
1421
|
if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
|
|
1386
1422
|
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
1387
1423
|
addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
|
|
@@ -1498,6 +1534,32 @@ function createDOMPurify() {
|
|
|
1498
1534
|
}
|
|
1499
1535
|
};
|
|
1500
1536
|
/**
|
|
1537
|
+
* _stripAttributeNode
|
|
1538
|
+
*
|
|
1539
|
+
* Remove a single Attr node case/namespace-exactly on an attribute-teardown
|
|
1540
|
+
* path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
|
|
1541
|
+
* HTML element in an HTML document and so silently misses a case-preserved
|
|
1542
|
+
* handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
|
|
1543
|
+
* _removeAttribute() was fixed for, which a name-based call would reintroduce
|
|
1544
|
+
* on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
|
|
1545
|
+
* record into DOMPurify.removed: the neutralize passes intentionally do not
|
|
1546
|
+
* book-keep. A clobbered/detached node falls back to best-effort name-based
|
|
1547
|
+
* removal.
|
|
1548
|
+
*
|
|
1549
|
+
* @param element the element to strip the attribute from
|
|
1550
|
+
* @param attribute the Attr node to remove
|
|
1551
|
+
* @param name the attribute's name, for the fallback path
|
|
1552
|
+
*/
|
|
1553
|
+
const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
|
|
1554
|
+
try {
|
|
1555
|
+
element.removeAttributeNode(attribute);
|
|
1556
|
+
} catch (_) {
|
|
1557
|
+
try {
|
|
1558
|
+
element.removeAttribute(name);
|
|
1559
|
+
} catch (_) {}
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
/**
|
|
1501
1563
|
* _neutralizeRoot
|
|
1502
1564
|
*
|
|
1503
1565
|
* Fail-closed teardown of an in-place root after the sanitize walk aborts
|
|
@@ -1531,30 +1593,45 @@ function createDOMPurify() {
|
|
|
1531
1593
|
if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1532
1594
|
const attribute = attributes[i];
|
|
1533
1595
|
const name = attribute && attribute.name;
|
|
1534
|
-
if (typeof name === "string")
|
|
1535
|
-
root.removeAttribute(name);
|
|
1536
|
-
} catch (_) {}
|
|
1596
|
+
if (typeof name === "string") _stripAttributeNode(root, attribute, name);
|
|
1537
1597
|
}
|
|
1538
1598
|
};
|
|
1539
1599
|
/**
|
|
1540
1600
|
* _removeAttribute
|
|
1541
1601
|
*
|
|
1602
|
+
* Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
|
|
1603
|
+
* lookup key for HTML elements in an HTML document, so they silently miss an
|
|
1604
|
+
* attribute whose stored qualified name still contains uppercase ASCII
|
|
1605
|
+
* letters. That happens when the node came from a case-preserving source
|
|
1606
|
+
* (an XML/XHTML document imported via importNode(), or createAttributeNS()),
|
|
1607
|
+
* where e.g. `ONERROR` survives the walk: the policy check lowercases to
|
|
1608
|
+
* `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
|
|
1609
|
+
* `onerror` and finds nothing. Remove the exact Attr node instead, which is
|
|
1610
|
+
* case- and namespace-exact, and fall back to name-based removal only when
|
|
1611
|
+
* the caller could not supply the node.
|
|
1612
|
+
*
|
|
1542
1613
|
* @param name an Attribute name
|
|
1543
1614
|
* @param element a DOM node
|
|
1615
|
+
* @param attr the exact Attr node to remove, when the caller has it
|
|
1544
1616
|
*/
|
|
1545
|
-
const _removeAttribute = function _removeAttribute(name, element) {
|
|
1617
|
+
const _removeAttribute = function _removeAttribute(name, element, attr) {
|
|
1618
|
+
if (!attr) try {
|
|
1619
|
+
attr = element.getAttributeNode(name);
|
|
1620
|
+
} catch (_) {
|
|
1621
|
+
attr = null;
|
|
1622
|
+
}
|
|
1623
|
+
arrayPush(DOMPurify.removed, {
|
|
1624
|
+
attribute: attr || null,
|
|
1625
|
+
from: element
|
|
1626
|
+
});
|
|
1546
1627
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
from: element
|
|
1550
|
-
});
|
|
1628
|
+
if (attr) element.removeAttributeNode(attr);
|
|
1629
|
+
else element.removeAttribute(name);
|
|
1551
1630
|
} catch (_) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
});
|
|
1631
|
+
try {
|
|
1632
|
+
element.removeAttribute(name);
|
|
1633
|
+
} catch (_) {}
|
|
1556
1634
|
}
|
|
1557
|
-
element.removeAttribute(name);
|
|
1558
1635
|
if (name === "is") if (RETURN_DOM || RETURN_DOM_FRAGMENT) try {
|
|
1559
1636
|
_forceRemove(element);
|
|
1560
1637
|
} catch (_) {}
|
|
@@ -1579,9 +1656,7 @@ function createDOMPurify() {
|
|
|
1579
1656
|
const attribute = attributes[i];
|
|
1580
1657
|
const name = attribute && attribute.name;
|
|
1581
1658
|
if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) continue;
|
|
1582
|
-
|
|
1583
|
-
element.removeAttribute(name);
|
|
1584
|
-
} catch (_) {}
|
|
1659
|
+
_stripAttributeNode(element, attribute, name);
|
|
1585
1660
|
}
|
|
1586
1661
|
};
|
|
1587
1662
|
/**
|
|
@@ -1610,7 +1685,7 @@ function createDOMPurify() {
|
|
|
1610
1685
|
const stack = [root];
|
|
1611
1686
|
while (stack.length > 0) {
|
|
1612
1687
|
const node = stack.pop();
|
|
1613
|
-
if ((
|
|
1688
|
+
if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
|
|
1614
1689
|
const childNodes = getChildNodes(node);
|
|
1615
1690
|
if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
|
|
1616
1691
|
}
|
|
@@ -1650,12 +1725,28 @@ function createDOMPurify() {
|
|
|
1650
1725
|
*
|
|
1651
1726
|
* @param root the in-place root to sweep
|
|
1652
1727
|
*/
|
|
1728
|
+
/**
|
|
1729
|
+
* Central policy for declarative-partial-updates patch-linkage attributes,
|
|
1730
|
+
* shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
|
|
1731
|
+
* the two sites cannot drift: `patchsrc` always links, `for` links
|
|
1732
|
+
* everywhere except on <label>/<output>, and the whole policy is gated on
|
|
1733
|
+
* SAFE_FOR_XML (see the rationale block in _isValidAttribute).
|
|
1734
|
+
*
|
|
1735
|
+
* @param lcName the transformCaseFunc'd attribute name
|
|
1736
|
+
* @param lcTag the transformCaseFunc'd tag name of the carrying element
|
|
1737
|
+
* @return true if the attribute is patch linkage and must be dropped
|
|
1738
|
+
*/
|
|
1739
|
+
const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
|
|
1740
|
+
if (!SAFE_FOR_XML) return false;
|
|
1741
|
+
if (lcName === "patchsrc") return true;
|
|
1742
|
+
return lcName === "for" && lcTag !== "label" && lcTag !== "output";
|
|
1743
|
+
};
|
|
1653
1744
|
const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
|
|
1654
1745
|
if (!SAFE_FOR_XML) return;
|
|
1655
1746
|
const stack = [root];
|
|
1656
1747
|
while (stack.length > 0) {
|
|
1657
1748
|
const node = stack.pop();
|
|
1658
|
-
const nodeType =
|
|
1749
|
+
const nodeType = _readNodeType(node);
|
|
1659
1750
|
if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
|
|
1660
1751
|
try {
|
|
1661
1752
|
remove(node);
|
|
@@ -1664,10 +1755,10 @@ function createDOMPurify() {
|
|
|
1664
1755
|
}
|
|
1665
1756
|
if (nodeType === NODE_TYPE.element) {
|
|
1666
1757
|
const element = node;
|
|
1667
|
-
const lcTag = transformCaseFunc(
|
|
1758
|
+
const lcTag = transformCaseFunc(_readNodeName(node));
|
|
1668
1759
|
try {
|
|
1669
1760
|
if (element.hasAttribute && element.hasAttribute("patchsrc")) element.removeAttribute("patchsrc");
|
|
1670
|
-
if (element.hasAttribute && element.hasAttribute("for") &&
|
|
1761
|
+
if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
|
|
1671
1762
|
} catch (_) {}
|
|
1672
1763
|
}
|
|
1673
1764
|
const childNodes = getChildNodes(node);
|
|
@@ -1836,12 +1927,32 @@ function createDOMPurify() {
|
|
|
1836
1927
|
*/
|
|
1837
1928
|
const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
|
|
1838
1929
|
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
|
|
1839
|
-
if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "
|
|
1930
|
+
if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) return true;
|
|
1840
1931
|
if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
|
|
1841
1932
|
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
|
|
1842
1933
|
return false;
|
|
1843
1934
|
};
|
|
1844
1935
|
/**
|
|
1936
|
+
* Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
|
|
1937
|
+
* function, per the validation in _parseConfig) against a name.
|
|
1938
|
+
* Additional arguments are forwarded to predicate functions - the
|
|
1939
|
+
* attributeNameCheck predicate receives the tag name as its second
|
|
1940
|
+
* argument. A null/absent check never matches.
|
|
1941
|
+
*
|
|
1942
|
+
* @param check the configured tagNameCheck / attributeNameCheck value
|
|
1943
|
+
* @param name the name to test
|
|
1944
|
+
* @param args extra arguments forwarded to a predicate function
|
|
1945
|
+
* @return true if the check matches the name
|
|
1946
|
+
*/
|
|
1947
|
+
const _matchesNameCheck = function _matchesNameCheck(check, name) {
|
|
1948
|
+
if (check instanceof RegExp) return regExpTest(check, name);
|
|
1949
|
+
if (check instanceof Function) {
|
|
1950
|
+
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) args[_key - 2] = arguments[_key];
|
|
1951
|
+
return Boolean(check(name, ...args));
|
|
1952
|
+
}
|
|
1953
|
+
return false;
|
|
1954
|
+
};
|
|
1955
|
+
/**
|
|
1845
1956
|
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1846
1957
|
* allowed custom elements (false return exits _sanitizeElements
|
|
1847
1958
|
* early - the namespace and fallback-tag removal checks are
|
|
@@ -1859,10 +1970,7 @@ function createDOMPurify() {
|
|
|
1859
1970
|
* @return true if the node was removed, false if kept
|
|
1860
1971
|
*/
|
|
1861
1972
|
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
|
1862
|
-
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName))
|
|
1863
|
-
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
|
|
1864
|
-
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) return false;
|
|
1865
|
-
}
|
|
1973
|
+
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
|
|
1866
1974
|
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
1867
1975
|
const parentNode = getParentNode(currentNode);
|
|
1868
1976
|
const childNodes = getChildNodes(currentNode);
|
|
@@ -1896,6 +2004,42 @@ function createDOMPurify() {
|
|
|
1896
2004
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
1897
2005
|
};
|
|
1898
2006
|
/**
|
|
2007
|
+
* Shared guard for a node that a hook has detached from the walk tree,
|
|
2008
|
+
* used after each element-hook site in _sanitizeElements. Detaching is a
|
|
2009
|
+
* long-standing user pattern (issue #469; draw.io-style foreignObject
|
|
2010
|
+
* filtering). Per the cached, unclobberable parentNode getter the node is
|
|
2011
|
+
* genuinely out of the tree, so it can reach neither the serialized
|
|
2012
|
+
* output nor an IN_PLACE live tree; treat it as removed and stop
|
|
2013
|
+
* processing it. Without this guard, the unsafe-node / namespace checks
|
|
2014
|
+
* would call _forceRemove on a parentless node and hit the REPORT-3
|
|
2015
|
+
* fail-closed throw — which exists for nodes DOMPurify wants gone but
|
|
2016
|
+
* *cannot* detach (clobbered / parentless roots), the opposite of a node
|
|
2017
|
+
* that is already safely gone. The walk root is exempt: a detached
|
|
2018
|
+
* IN_PLACE root is legitimate input and must still be fully sanitized,
|
|
2019
|
+
* and a kill-decision on it must keep hitting the REPORT-3 throw.
|
|
2020
|
+
*
|
|
2021
|
+
* Nodes detached by hooks stay the hook's responsibility for placement:
|
|
2022
|
+
* they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
|
|
2023
|
+
* pass (which iterates DOMPurify.removed) does not reach them. But a
|
|
2024
|
+
* hook-detached subtree can still hold a queued resource-event handler -
|
|
2025
|
+
* e.g. an <img onload> that began loading when the caller built the live
|
|
2026
|
+
* tree - which fires in page scope after sanitize returns even though the
|
|
2027
|
+
* handler never reached the returned tree. That is the audit-5 F1 hazard,
|
|
2028
|
+
* and the documented node.remove() hook pattern walks straight into it.
|
|
2029
|
+
* So on the IN_PLACE path we neutralize the detached subtree inline,
|
|
2030
|
+
* stripping its non-allow-listed attributes before returning, exactly as
|
|
2031
|
+
* the post-walk pass does for _forceRemove'd subtrees.
|
|
2032
|
+
*
|
|
2033
|
+
* @param currentNode the node a hook may have detached
|
|
2034
|
+
* @param root the current walk root
|
|
2035
|
+
* @return true if the node is detached and now handled, false otherwise
|
|
2036
|
+
*/
|
|
2037
|
+
const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
|
|
2038
|
+
if (currentNode === root || getParentNode(currentNode) !== null) return false;
|
|
2039
|
+
if (IN_PLACE) _neutralizeSubtree(currentNode);
|
|
2040
|
+
return true;
|
|
2041
|
+
};
|
|
2042
|
+
/**
|
|
1899
2043
|
* _sanitizeElements
|
|
1900
2044
|
*
|
|
1901
2045
|
* @protect nodeName
|
|
@@ -1906,24 +2050,18 @@ function createDOMPurify() {
|
|
|
1906
2050
|
*/
|
|
1907
2051
|
const _sanitizeElements = function _sanitizeElements(currentNode, root) {
|
|
1908
2052
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1909
|
-
if (currentNode
|
|
1910
|
-
if (IN_PLACE) _neutralizeSubtree(currentNode);
|
|
1911
|
-
return true;
|
|
1912
|
-
}
|
|
2053
|
+
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
1913
2054
|
if (_isClobbered(currentNode)) {
|
|
1914
2055
|
_forceRemove(currentNode);
|
|
1915
2056
|
return true;
|
|
1916
2057
|
}
|
|
1917
|
-
const tagName = transformCaseFunc(
|
|
2058
|
+
const tagName = transformCaseFunc(_readNodeName(currentNode));
|
|
1918
2059
|
ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
|
|
1919
2060
|
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
1920
2061
|
tagName,
|
|
1921
2062
|
allowedTags: ALLOWED_TAGS
|
|
1922
2063
|
});
|
|
1923
|
-
if (currentNode
|
|
1924
|
-
if (IN_PLACE) _neutralizeSubtree(currentNode);
|
|
1925
|
-
return true;
|
|
1926
|
-
}
|
|
2064
|
+
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
1927
2065
|
if (_isUnsafeNode(currentNode, tagName)) {
|
|
1928
2066
|
_forceRemove(currentNode);
|
|
1929
2067
|
return true;
|
|
@@ -1933,7 +2071,7 @@ function createDOMPurify() {
|
|
|
1933
2071
|
if (removed === false) _executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
1934
2072
|
return removed;
|
|
1935
2073
|
}
|
|
1936
|
-
if ((
|
|
2074
|
+
if (_readNodeType(currentNode) === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
|
|
1937
2075
|
_forceRemove(currentNode);
|
|
1938
2076
|
return true;
|
|
1939
2077
|
}
|
|
@@ -1961,20 +2099,17 @@ function createDOMPurify() {
|
|
|
1961
2099
|
*/
|
|
1962
2100
|
const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
|
|
1963
2101
|
if (FORBID_ATTR[lcName]) return false;
|
|
1964
|
-
if (
|
|
1965
|
-
if (SAFE_FOR_XML && lcName === "for" && lcTag !== "label" && lcTag !== "output") return false;
|
|
2102
|
+
if (_isPatchLinkageAttribute(lcName, lcTag)) return false;
|
|
1966
2103
|
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) return false;
|
|
1967
2104
|
const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
|
|
1968
|
-
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName));
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
else if (value) return false;
|
|
1977
|
-
return true;
|
|
2105
|
+
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) return true;
|
|
2106
|
+
if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) return true;
|
|
2107
|
+
if (!nameIsPermitted) return _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value);
|
|
2108
|
+
if (URI_SAFE_ATTRIBUTES[lcName]) return true;
|
|
2109
|
+
if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
|
|
2110
|
+
if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) return true;
|
|
2111
|
+
if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) return true;
|
|
2112
|
+
return !value;
|
|
1978
2113
|
};
|
|
1979
2114
|
const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, [
|
|
1980
2115
|
"annotation-xml",
|
|
@@ -2076,29 +2211,29 @@ function createDOMPurify() {
|
|
|
2076
2211
|
_executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
|
|
2077
2212
|
value = hookEvent.attrValue;
|
|
2078
2213
|
if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
|
|
2079
|
-
_removeAttribute(name, currentNode);
|
|
2214
|
+
_removeAttribute(name, currentNode, attr);
|
|
2080
2215
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
2081
2216
|
}
|
|
2082
2217
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
|
|
2083
|
-
_removeAttribute(name, currentNode);
|
|
2218
|
+
_removeAttribute(name, currentNode, attr);
|
|
2084
2219
|
continue;
|
|
2085
2220
|
}
|
|
2086
2221
|
if (lcName === "attributename" && stringMatch(value, "href")) {
|
|
2087
|
-
_removeAttribute(name, currentNode);
|
|
2222
|
+
_removeAttribute(name, currentNode, attr);
|
|
2088
2223
|
continue;
|
|
2089
2224
|
}
|
|
2090
2225
|
if (hookEvent.forceKeepAttr) continue;
|
|
2091
2226
|
if (!hookEvent.keepAttr) {
|
|
2092
|
-
_removeAttribute(name, currentNode);
|
|
2227
|
+
_removeAttribute(name, currentNode, attr);
|
|
2093
2228
|
continue;
|
|
2094
2229
|
}
|
|
2095
2230
|
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
|
|
2096
|
-
_removeAttribute(name, currentNode);
|
|
2231
|
+
_removeAttribute(name, currentNode, attr);
|
|
2097
2232
|
continue;
|
|
2098
2233
|
}
|
|
2099
2234
|
if (SAFE_FOR_TEMPLATES) value = _stripTemplateExpressions(value);
|
|
2100
2235
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
2101
|
-
_removeAttribute(name, currentNode);
|
|
2236
|
+
_removeAttribute(name, currentNode, attr);
|
|
2102
2237
|
continue;
|
|
2103
2238
|
}
|
|
2104
2239
|
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
@@ -2120,7 +2255,7 @@ function createDOMPurify() {
|
|
|
2120
2255
|
_sanitizeElements(shadowNode, fragment);
|
|
2121
2256
|
_sanitizeAttributes(shadowNode);
|
|
2122
2257
|
if (_isDocumentFragment(shadowNode.content)) _sanitizeShadowDOM2(shadowNode.content);
|
|
2123
|
-
if ((
|
|
2258
|
+
if (_readNodeType(shadowNode) === NODE_TYPE.element) {
|
|
2124
2259
|
const innerSr = getShadowRoot(shadowNode);
|
|
2125
2260
|
if (_isDocumentFragment(innerSr)) {
|
|
2126
2261
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
@@ -2161,7 +2296,7 @@ function createDOMPurify() {
|
|
|
2161
2296
|
continue;
|
|
2162
2297
|
}
|
|
2163
2298
|
const node = item.node;
|
|
2164
|
-
const isElement = (
|
|
2299
|
+
const isElement = _readNodeType(node) === NODE_TYPE.element;
|
|
2165
2300
|
const childNodes = getChildNodes(node);
|
|
2166
2301
|
if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push({
|
|
2167
2302
|
node: childNodes[i],
|
|
@@ -2212,7 +2347,7 @@ function createDOMPurify() {
|
|
|
2212
2347
|
const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
|
|
2213
2348
|
if (inPlace) {
|
|
2214
2349
|
_neutralizePatchLinkage(dirty);
|
|
2215
|
-
const nn =
|
|
2350
|
+
const nn = _readNodeName(dirty);
|
|
2216
2351
|
if (typeof nn === "string") {
|
|
2217
2352
|
const tagName = transformCaseFunc(nn);
|
|
2218
2353
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|