tinymce-rails 8.8.1 → 8.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a62c7a288ed5696f584343feec82754519985ae2f5b114d512cad2dfbdeee0ad
4
- data.tar.gz: afa1237efdcf85f4b879e2d857ebaf5c6a76a27b11861aae0cf853337da4ac48
3
+ metadata.gz: 0c76539b0f4c153f018da0a4c42acce9a1dcea31efb03dabc9f44d1e1d61b3ec
4
+ data.tar.gz: b0ec30ffc6de0ffa5fcd0720388ef73f7ca5af1c966baac68e911c9699a59a29
5
5
  SHA512:
6
- metadata.gz: 1f34f17bd042bb908ff1551f4db673326eae1b476555866c15c58f837f0141431c3c596d302088763879b3ac34f1d3af77f136189cf8d4dd55a36c012b07c9ec
7
- data.tar.gz: 96a89c0c643a51a2b904de5c4dd4f45c20abb0e3572f3613f44edf54dddc2aa44816b29c900500e9c39ec9b2a30309e2a9364f5265303362feb428b2db5f4c0f
6
+ metadata.gz: 3229b680d9cb17075ef7686f230ebc0e5bc3ae090e4623c28c025ecd98c96cc9fa537d1a35010baceb03d365ae6590babaad2dce34f3f6ee53e0a25d47b6d94f
7
+ data.tar.gz: 03ac9f068a069403e8c8d3df13172a213794fd1cba3d63a1d703372919bd9873a80f2bf1180587dac4f1943e7c290fc182e71f69fa88e5b9c4f718284b506323
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TinyMCE version 8.8.1 (2026-07-22)
2
+ * TinyMCE version 8.8.2 (2026-07-27)
3
3
  */
4
4
 
5
5
  (function () {
@@ -12153,6 +12153,13 @@
12153
12153
  const selector = isEmpty$5(formatNoneditableSelector) ? baseDataSelector : `${baseDataSelector},${formatNoneditableSelector}`;
12154
12154
  return is$2(SugarElement.fromDom(node), selector);
12155
12155
  };
12156
+ const hasEditableDescendants = (dom, node) => dom.select('[contenteditable="true"]', node).length > 0;
12157
+ const getEditableDescendants = (dom, parent) => foldl(from(parent.childNodes), (editableDescendants, child) => [
12158
+ ...editableDescendants,
12159
+ ...(isElement$7(child) && dom.getContentEditable(child) === 'true'
12160
+ ? [child]
12161
+ : getEditableDescendants(dom, child))
12162
+ ], []);
12156
12163
  // A noneditable element is wrappable if it:
12157
12164
  // - is valid target (has data-mce-cef-wrappable attribute or matches selector from option)
12158
12165
  // - has no editable descendants - removing formats in the editable region can result in the wrapped noneditable being split which is undesirable
@@ -12161,7 +12168,7 @@
12161
12168
  return (isElementNode$1(node) &&
12162
12169
  dom.getContentEditable(node) === 'false' &&
12163
12170
  isWrapNoneditableTarget(editor, node) &&
12164
- dom.select('[contenteditable="true"]', node).length === 0);
12171
+ !hasEditableDescendants(dom, node));
12165
12172
  };
12166
12173
  /**
12167
12174
  * Replaces variables in the value. The variable format is %var.
@@ -16769,14 +16776,23 @@
16769
16776
  }
16770
16777
  return undefined;
16771
16778
  };
16779
+ const containsFormatDeep = (editor, root, name, vars, similar) => isNonNullable(matchNode$1(editor, root, name, vars, similar)) ||
16780
+ exists(from(root.childNodes), (child) => containsFormatDeep(editor, child, name, vars, similar));
16781
+ const matchEditableDescendants = (editor, node, name, vars, similar) => {
16782
+ const dom = editor.dom;
16783
+ if (!isElement$7(node) || dom.getContentEditable(node) !== 'false' || isWrappableNoneditable(editor, node)) {
16784
+ return false;
16785
+ }
16786
+ return exists(getEditableDescendants(dom, node), (descendant) => containsFormatDeep(editor, descendant, name, vars, similar));
16787
+ };
16772
16788
  const match$2 = (editor, name, vars, node, similar) => {
16773
16789
  // Check specified node
16774
16790
  if (node) {
16775
- return matchParents(editor, node, name, vars, similar);
16791
+ return matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar);
16776
16792
  }
16777
16793
  // Check selected node
16778
16794
  node = editor.selection.getNode();
16779
- if (matchParents(editor, node, name, vars, similar)) {
16795
+ if (matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar)) {
16780
16796
  return true;
16781
16797
  }
16782
16798
  // Check start node if it's different
@@ -18647,7 +18663,7 @@
18647
18663
  }
18648
18664
  };
18649
18665
 
18650
- /*! @license DOMPurify 3.4.11 | (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.11/LICENSE */
18666
+ /*! @license DOMPurify 3.4.12 | (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.12/LICENSE */
18651
18667
 
18652
18668
  function _arrayLikeToArray(r, a) {
18653
18669
  (null == a || a > r.length) && (a = r.length);
@@ -18960,7 +18976,7 @@
18960
18976
  const text = freeze(['#text']);
18961
18977
 
18962
18978
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
18963
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
18979
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
18964
18980
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
18965
18981
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
18966
18982
 
@@ -19073,7 +19089,7 @@
19073
19089
  function createDOMPurify() {
19074
19090
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
19075
19091
  const DOMPurify = root => createDOMPurify(root);
19076
- DOMPurify.version = '3.4.11';
19092
+ DOMPurify.version = '3.4.12';
19077
19093
  DOMPurify.removed = [];
19078
19094
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
19079
19095
  // Not running in a browser, provide a factory function
@@ -19755,6 +19771,13 @@
19755
19771
  * @param root the in-place root to empty
19756
19772
  */
19757
19773
  const _neutralizeRoot = function _neutralizeRoot(root) {
19774
+ /* Strip every disallowed attribute (on* handlers included) off the whole
19775
+ subtree BEFORE detaching anything. Detaching first would hand back
19776
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
19777
+ whose queued resource event still fires in page scope after we throw.
19778
+ Clobber-safe reads; a doomed clobbered node's own attributes are
19779
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
19780
+ _neutralizeSubtree(root);
19758
19781
  const childNodes = getChildNodes(root);
19759
19782
  if (childNodes) {
19760
19783
  const snapshot = [];
@@ -19882,6 +19905,82 @@
19882
19905
  }
19883
19906
  }
19884
19907
  };
19908
+ /**
19909
+ * _neutralizePatchLinkage
19910
+ *
19911
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
19912
+ * hardening, https://github.com/WICG/declarative-partial-updates).
19913
+ *
19914
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
19915
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
19916
+ * reaches each node. On a live in-place root that leaves a window: from the
19917
+ * moment the root is connected until the walk arrives at a given node, that
19918
+ * node's linkage is live. A patch applied on connection/stream can fire as
19919
+ * a microtask during the walk and inject or teleport an unsanitized DOM
19920
+ * range into a region the iterator has already passed and will not revisit,
19921
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
19922
+ * whole tree once up front and sever every linkage before the walk begins,
19923
+ * closing that window.
19924
+ *
19925
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
19926
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
19927
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
19928
+ * to group with the rest of the declarative-partial-updates handling and
19929
+ * stay overridable, consistent with the codebase.
19930
+ *
19931
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
19932
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
19933
+ *
19934
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
19935
+ * .html Q1): this mirrors the existing policy of keeping `for` on
19936
+ * <label>/<output>. If the shipping feature can drive a patch through a
19937
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
19938
+ * attribute check at _isBasicCustomElement's caller must additionally drop
19939
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
19940
+ *
19941
+ * @param root the in-place root to sweep
19942
+ */
19943
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
19944
+ if (!SAFE_FOR_XML) {
19945
+ return;
19946
+ }
19947
+ const stack = [root];
19948
+ while (stack.length > 0) {
19949
+ const node = stack.pop();
19950
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
19951
+ /* Remove range markers (the target side of a patch linkage): every
19952
+ processing instruction, and any markup-bearing comment. */
19953
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
19954
+ try {
19955
+ remove(node);
19956
+ } catch (_) {
19957
+ /* Best-effort */
19958
+ }
19959
+ continue;
19960
+ }
19961
+ /* Strip patch-source attributes (the source side) off elements. */
19962
+ if (nodeType === NODE_TYPE.element) {
19963
+ const element = node;
19964
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
19965
+ try {
19966
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
19967
+ element.removeAttribute('patchsrc');
19968
+ }
19969
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
19970
+ element.removeAttribute('for');
19971
+ }
19972
+ } catch (_) {
19973
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
19974
+ }
19975
+ }
19976
+ const childNodes = getChildNodes(node);
19977
+ if (childNodes) {
19978
+ for (let i = childNodes.length - 1; i >= 0; --i) {
19979
+ stack.push(childNodes[i]);
19980
+ }
19981
+ }
19982
+ }
19983
+ };
19885
19984
  /**
19886
19985
  * _initDocument
19887
19986
  *
@@ -20132,9 +20231,15 @@
20132
20231
  /**
20133
20232
  * Handle a node whose tag is forbidden or not allowlisted: keep
20134
20233
  * allowed custom elements (false return exits _sanitizeElements
20135
- * early - namespace/fallback checks and the afterSanitizeElements
20136
- * hook are intentionally skipped for kept custom elements), else
20137
- * hoist content per KEEP_CONTENT and remove.
20234
+ * early - the namespace and fallback-tag removal checks are
20235
+ * intentionally skipped for kept custom elements), else hoist
20236
+ * content per KEEP_CONTENT and remove.
20237
+ *
20238
+ * A kept custom element is the ONLY case in which this function
20239
+ * returns false, so the caller uses that return value to run the
20240
+ * afterSanitizeElements hook on the kept element and keep the
20241
+ * element-hook lifecycle consistent with normal allowlisted
20242
+ * elements (GHSA-c2j3-45gr-mqc4).
20138
20243
  *
20139
20244
  * @param currentNode the disallowed node
20140
20245
  * @param tagName the node's transformCaseFunc'd tag name
@@ -20200,9 +20305,15 @@
20200
20305
  * @param currentNode to check for permission to exist
20201
20306
  * @return true if node was killed, false if left alive
20202
20307
  */
20203
- const _sanitizeElements = function _sanitizeElements(currentNode) {
20308
+ // eslint-disable-next-line complexity
20309
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
20204
20310
  /* Execute a hook if present */
20205
20311
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
20312
+ /* A hook may have detached the node — treat it as removed (see the
20313
+ detached-node comment after the uponSanitizeElement hook below). */
20314
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20315
+ return true;
20316
+ }
20206
20317
  /* Check if element is clobbered or can clobber */
20207
20318
  if (_isClobbered(currentNode)) {
20208
20319
  _forceRemove(currentNode);
@@ -20215,6 +20326,24 @@
20215
20326
  tagName,
20216
20327
  allowedTags: ALLOWED_TAGS
20217
20328
  });
20329
+ /* A hook may have detached the node from the tree — a long-standing
20330
+ user pattern (issue #469; draw.io-style foreignObject filtering).
20331
+ Per the cached, unclobberable parentNode getter the node is
20332
+ genuinely out of the tree, so it can reach neither the serialized
20333
+ output nor an IN_PLACE live tree; treat it as removed and stop
20334
+ processing it. Without this guard, the unsafe-node / namespace
20335
+ checks below would call _forceRemove on a parentless node and hit
20336
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
20337
+ wants gone but *cannot* detach (clobbered / parentless roots), the
20338
+ opposite of a node that is already safely gone. The walk root is
20339
+ exempt: a detached IN_PLACE root is legitimate input and must still
20340
+ be fully sanitized, and a kill-decision on it must keep hitting the
20341
+ REPORT-3 throw. Nodes detached by hooks are the hook's
20342
+ responsibility: they are not recorded in DOMPurify.removed and are
20343
+ not neutralized by the post-walk IN_PLACE pass. */
20344
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20345
+ return true;
20346
+ }
20218
20347
  /* Remove mXSS vectors, processing instructions and risky comments */
20219
20348
  if (_isUnsafeNode(currentNode, tagName)) {
20220
20349
  _forceRemove(currentNode);
@@ -20222,7 +20351,22 @@
20222
20351
  }
20223
20352
  /* Remove element if anything forbids its presence */
20224
20353
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
20225
- return _sanitizeDisallowedNode(currentNode, tagName);
20354
+ const removed = _sanitizeDisallowedNode(currentNode, tagName);
20355
+ /* A false return means the node is a custom element kept via
20356
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
20357
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
20358
+ element-hook lifecycle matches normal allowlisted elements: a
20359
+ security policy applied in this hook (e.g. stripping an attribute
20360
+ from every surviving element) must not silently skip kept custom
20361
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
20362
+ tail below - the hook runs, then the walker's subsequent
20363
+ _sanitizeAttributes pass sanitizes the element's attributes. The
20364
+ deliberately skipped namespace and fallback-tag removal checks stay
20365
+ skipped; they are removal decisions, not the hook contract. */
20366
+ if (removed === false) {
20367
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
20368
+ }
20369
+ return removed;
20226
20370
  }
20227
20371
  /* Check whether element has a valid namespace.
20228
20372
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -20269,6 +20413,33 @@
20269
20413
  if (FORBID_ATTR[lcName]) {
20270
20414
  return false;
20271
20415
  }
20416
+ /* Reject declarative-partial-updates patch-linkage attributes
20417
+ (https://github.com/WICG/declarative-partial-updates).
20418
+ Empirical note (Chrome 150, verified — see
20419
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
20420
+ sanitization. For the string path it fires during sanitize()'s own
20421
+ parse, so the walk sees and sanitizes the fully materialized expanded
20422
+ tree — teleports into MathML/SVG integration points included; a
20423
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
20424
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
20425
+ Either way DOMPurify is NOT blind to the patch.
20426
+ This removal is therefore defense-in-depth rather than the sole barrier:
20427
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
20428
+ in the caller's context, and keeps behaviour deterministic if a future
20429
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
20430
+ anywhere else (notably <template for>) it links the element to a patch
20431
+ target and teleports or removes an arbitrary DOM range by id/marker name.
20432
+ `patchsrc` fetches remote markup and is treated as a script-loading
20433
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
20434
+ other structural-threat checks and stays overridable, consistent with
20435
+ the rest of the codebase. PI range markers are already removed by
20436
+ _isUnsafeNode. */
20437
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
20438
+ return false;
20439
+ }
20440
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
20441
+ return false;
20442
+ }
20272
20443
  /* Make sure attribute cannot clobber */
20273
20444
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
20274
20445
  return false;
@@ -20478,7 +20649,7 @@
20478
20649
  /* Execute a hook if present */
20479
20650
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
20480
20651
  /* Sanitize tags and elements */
20481
- _sanitizeElements(shadowNode);
20652
+ _sanitizeElements(shadowNode, fragment);
20482
20653
  /* Check attributes next */
20483
20654
  _sanitizeAttributes(shadowNode);
20484
20655
  /* Deep shadow DOM detected.
@@ -20669,6 +20840,11 @@
20669
20840
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
20670
20841
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
20671
20842
  if (inPlace) {
20843
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
20844
+ linkage across the live tree BEFORE the walk, so no patch can fire
20845
+ mid-walk and inject into an already-processed region. Runs first, so
20846
+ it also covers the forbidden/clobbered roots that throw below. */
20847
+ _neutralizePatchLinkage(dirty);
20672
20848
  /* Do some early pre-sanitization to avoid unsafe root nodes.
20673
20849
  Read nodeName through the cached prototype getter — a clobbering
20674
20850
  child named "nodeName" on the form root would otherwise shadow
@@ -20678,6 +20854,9 @@
20678
20854
  if (typeof nn === 'string') {
20679
20855
  const tagName = transformCaseFunc(nn);
20680
20856
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
20857
+ /* Fail closed on a live root: neutralize handlers/children before
20858
+ throwing, exactly as the mid-walk abort path does. */
20859
+ _neutralizeRoot(dirty);
20681
20860
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
20682
20861
  }
20683
20862
  }
@@ -20692,6 +20871,10 @@
20692
20871
  the application unsanitized. Refuse to sanitize such a root
20693
20872
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
20694
20873
  if (_isClobbered(dirty)) {
20874
+ /* Fail closed on a live clobbered root before throwing.
20875
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
20876
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
20877
+ _neutralizeRoot(dirty);
20695
20878
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
20696
20879
  }
20697
20880
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -20744,7 +20927,8 @@
20744
20927
  _forceRemove(body.firstChild);
20745
20928
  }
20746
20929
  /* Get node iterator */
20747
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
20930
+ const walkRoot = inPlace ? dirty : body;
20931
+ const nodeIterator = _createNodeIterator(walkRoot);
20748
20932
  /* Now start iterating over the created document.
20749
20933
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
20750
20934
  engine/custom-element mutation can detach a node mid-walk so
@@ -20757,7 +20941,7 @@
20757
20941
  try {
20758
20942
  while (currentNode = nodeIterator.nextNode()) {
20759
20943
  /* Sanitize tags and elements */
20760
- _sanitizeElements(currentNode);
20944
+ _sanitizeElements(currentNode, walkRoot);
20761
20945
  /* Check attributes next */
20762
20946
  _sanitizeAttributes(currentNode);
20763
20947
  /* Shadow DOM detected, sanitize it.
@@ -20771,6 +20955,14 @@
20771
20955
  } catch (error) {
20772
20956
  if (inPlace) {
20773
20957
  _neutralizeRoot(dirty);
20958
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
20959
+ detached from the root, so _neutralizeRoot's subtree pass does not
20960
+ reach them. Defuse them too, mirroring the success-path loop below. */
20961
+ arrayForEach(DOMPurify.removed, entry => {
20962
+ if (entry.element) {
20963
+ _neutralizeSubtree(entry.element);
20964
+ }
20965
+ });
20774
20966
  }
20775
20967
  throw error;
20776
20968
  }
@@ -21368,44 +21560,22 @@
21368
21560
  const bogus = get$a(element, 'data-mce-bogus');
21369
21561
  if (!isInternalElement && isString(bogus)) {
21370
21562
  if (bogus === 'all') {
21371
- // Empty before removing so a detached raw-text element (e.g. script/style) can't carry
21372
- // markup-like content into DOMPurify's unsafe-node check, which would then throw trying
21373
- // to _forceRemove the now-parentless node.
21374
- empty(element);
21375
21563
  remove$8(element);
21376
21564
  }
21377
21565
  else {
21378
21566
  unwrap(element);
21379
21567
  }
21380
- // We detach the element above; DOMPurify throws if it then tries to _forceRemove the
21381
- // now-parentless node, so mark the tag allowed to stop that second removal.
21382
- if (isNonNullable(evt)) {
21383
- evt.allowedTags[lcTagName] = true;
21384
- }
21385
21568
  return;
21386
21569
  }
21387
21570
  // Determine if the schema allows the element and either add it or remove it
21388
21571
  const rule = schema.getElementRule(lcTagName);
21389
21572
  if (validate && !rule) {
21390
- // Don't detach invalid special / non-HTML-namespace elements: DOMPurify throws if it later
21391
- // _forceRemoves a parentless node (its unsafe-content and namespace checks do this regardless
21392
- // of allowedTags). Empty and leave attached instead, so DOMPurify removes it without hoisting content.
21393
- if (settings.sanitize && (has$2(specialElements, lcTagName) || scope !== 'html')) {
21394
- empty(element);
21573
+ // If a special element is invalid, then remove the entire element instead of unwrapping
21574
+ if (has$2(specialElements, lcTagName)) {
21575
+ remove$8(element);
21395
21576
  }
21396
21577
  else {
21397
- // No DOMPurify to remove it (sanitize disabled), or a plain invalid HTML element: detach it ourselves.
21398
- if (has$2(specialElements, lcTagName)) {
21399
- remove$8(element);
21400
- }
21401
- else {
21402
- unwrap(element);
21403
- }
21404
- // The element is now detached; mark the tag allowed so DOMPurify won't _forceRemove the
21405
- // parentless node and throw. Safe here: it's empty and HTML-namespaced, so no earlier check fires.
21406
- if (isNonNullable(evt)) {
21407
- evt.allowedTags[lcTagName] = true;
21408
- }
21578
+ unwrap(element);
21409
21579
  }
21410
21580
  return;
21411
21581
  }
@@ -21588,9 +21758,7 @@
21588
21758
  evt.allowedTags[lcTagName] = keepElement;
21589
21759
  if (!keepElement && settings.sanitize) {
21590
21760
  if (isElement$7(node)) {
21591
- // Empty the node and leave it attached so DOMPurify removes it (allowedTags is false).
21592
- // Detaching it would make DOMPurify throw when it _forceRemoves the parentless node.
21593
- empty(SugarElement.fromDom(node));
21761
+ node.remove();
21594
21762
  }
21595
21763
  }
21596
21764
  });
@@ -23212,14 +23380,21 @@
23212
23380
  }
23213
23381
  });
23214
23382
  };
23215
- // TODO: TINY-9142: Remove this to make nested noneditable formatting work
23216
23383
  const targetNode = isNode(node) ? node : selection.getNode();
23217
23384
  if (dom.getContentEditable(targetNode) === 'false' && !isWrappableNoneditable(ed, targetNode)) {
23218
23385
  // node variable is used by other functions above in the same scope so need to set it here
23219
23386
  node = targetNode;
23220
23387
  applyNodeStyle(formatList, node);
23221
- if (isBlockFormat(format) && !dom.isBlock(targetNode)) {
23222
- const parentBlock = dom.getParent(targetNode, dom.isBlock);
23388
+ if (hasEditableDescendants(dom, node)) {
23389
+ each$e(getEditableDescendants(dom, node), (island) => {
23390
+ const rng = dom.createRng();
23391
+ rng.selectNodeContents(island);
23392
+ applyRngStyle(dom, rng, true);
23393
+ });
23394
+ return;
23395
+ }
23396
+ if (isBlockFormat(format) && !dom.isBlock(node)) {
23397
+ const parentBlock = dom.getParent(node, dom.isBlock);
23223
23398
  if (dom.isEditable(parentBlock)) {
23224
23399
  const wrapperElementName = format.block;
23225
23400
  if (parentBlock.nodeName.toLowerCase() === wrapperElementName.toLowerCase()) {
@@ -23231,6 +23406,12 @@
23231
23406
  }
23232
23407
  }
23233
23408
  }
23409
+ else {
23410
+ const selectorFormats = filter$5(formatList, isSelectorFormat);
23411
+ Optional.from(dom.getParent(node, (candidate) => exists(selectorFormats, (fmt) => dom.is(candidate, fmt.selector))))
23412
+ .filter(dom.isEditable)
23413
+ .each((parent) => applyNodeStyle(formatList, parent));
23414
+ }
23234
23415
  fireFormatApply(ed, name, node, vars);
23235
23416
  return;
23236
23417
  }
@@ -41901,14 +42082,14 @@
41901
42082
  * @property minorVersion
41902
42083
  * @type String
41903
42084
  */
41904
- minorVersion: '8.1',
42085
+ minorVersion: '8.2',
41905
42086
  /**
41906
42087
  * Release date of TinyMCE build.
41907
42088
  *
41908
42089
  * @property releaseDate
41909
42090
  * @type String
41910
42091
  */
41911
- releaseDate: '2026-07-22',
42092
+ releaseDate: '2026-07-27',
41912
42093
  /**
41913
42094
  * Collection of language pack data.
41914
42095
  *
@@ -1,6 +1,6 @@
1
1
  module TinyMCE
2
2
  module Rails
3
- VERSION = "8.8.1"
4
- TINYMCE_VERSION = "8.8.1"
3
+ VERSION = "8.8.2"
4
+ TINYMCE_VERSION = "8.8.2"
5
5
  end
6
6
  end