@xmldom/xmldom 0.8.3 → 0.8.5

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.8.5](https://github.com/xmldom/xmldom/compare/0.8.4...0.8.5)
8
+
9
+ ### Fixed
10
+
11
+ - fix: Restore ES5 compatibility [`#452`](https://github.com/xmldom/xmldom/pull/452) / [`#453`](https://github.com/xmldom/xmldom/issues/453)
12
+
13
+ Thank you, [@fengxinming](https://github.com/fengxinming), for your contributions
14
+
15
+
16
+ ## [0.8.4](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.4)
17
+
18
+ ### Fixed
19
+
20
+ - Security: Prevent inserting DOM nodes when they are not well-formed [`CVE-2022-39353`](https://github.com/xmldom/xmldom/security/advisories/GHSA-crh6-fp67-6883)
21
+ In case such a DOM would be created, the part that is not well-formed will be transformed into text nodes, in which xml specific characters like `<` and `>` are encoded accordingly.
22
+ In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead.
23
+ This change can break your code, if you relied on this behavior, e.g. multiple root elements in the past. We consider it more important to align with the specs that we want to be aligned with, considering the potential security issues that might derive from people not being aware of the difference in behavior.
24
+ Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity>
25
+
26
+ Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
27
+
28
+
7
29
  ## [0.8.3](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.2)
8
30
 
9
31
  ### Fixed
@@ -1,5 +1,37 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * Ponyfill for `Array.prototype.find` which is only available in ES6 runtimes.
5
+ *
6
+ * Works with anything that has a `length` property and index access properties, including NodeList.
7
+ *
8
+ * @template {unknown} T
9
+ * @param {Array<T> | ({length:number, [number]: T})} list
10
+ * @param {function (item: T, index: number, list:Array<T> | ({length:number, [number]: T})):boolean} predicate
11
+ * @param {Partial<Pick<ArrayConstructor['prototype'], 'find'>>?} ac `Array.prototype` by default,
12
+ * allows injecting a custom implementation in tests
13
+ * @returns {T | undefined}
14
+ *
15
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
16
+ * @see https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.find
17
+ */
18
+ function find(list, predicate, ac) {
19
+ if (ac === undefined) {
20
+ ac = Array.prototype;
21
+ }
22
+ if (list && typeof ac.find === 'function') {
23
+ return ac.find.call(list, predicate);
24
+ }
25
+ for (var i = 0; i < list.length; i++) {
26
+ if (Object.prototype.hasOwnProperty.call(list, i)) {
27
+ var item = list[i];
28
+ if (predicate.call(undefined, item, i, list)) {
29
+ return item;
30
+ }
31
+ }
32
+ }
33
+ }
34
+
3
35
  /**
4
36
  * "Shallow freezes" an object to render it immutable.
5
37
  * Uses `Object.freeze` if available,
@@ -165,6 +197,7 @@ var NAMESPACE = freeze({
165
197
  })
166
198
 
167
199
  exports.assign = assign;
200
+ exports.find = find;
168
201
  exports.freeze = freeze;
169
202
  exports.MIME_TYPE = MIME_TYPE;
170
203
  exports.NAMESPACE = NAMESPACE;
package/lib/dom.js CHANGED
@@ -1,5 +1,6 @@
1
1
  var conventions = require("./conventions");
2
2
 
3
+ var find = conventions.find;
3
4
  var NAMESPACE = conventions.NAMESPACE;
4
5
 
5
6
  /**
@@ -158,14 +159,14 @@ NodeList.prototype = {
158
159
  * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive.
159
160
  * @standard level1
160
161
  */
161
- length:0,
162
+ length:0,
162
163
  /**
163
164
  * Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null.
164
165
  * @standard level1
165
- * @param index unsigned long
166
+ * @param index unsigned long
166
167
  * Index into the collection.
167
168
  * @return Node
168
- * The node at the indexth position in the NodeList, or null if that is not a valid index.
169
+ * The node at the indexth position in the NodeList, or null if that is not a valid index.
169
170
  */
170
171
  item: function(index) {
171
172
  return this[index] || null;
@@ -175,7 +176,23 @@ NodeList.prototype = {
175
176
  serializeToString(this[i],buf,isHTML,nodeFilter);
176
177
  }
177
178
  return buf.join('');
178
- }
179
+ },
180
+ /**
181
+ * @private
182
+ * @param {function (Node):boolean} predicate
183
+ * @returns {Node[]}
184
+ */
185
+ filter: function (predicate) {
186
+ return Array.prototype.filter.call(this, predicate);
187
+ },
188
+ /**
189
+ * @private
190
+ * @param {Node} item
191
+ * @returns {number}
192
+ */
193
+ indexOf: function (item) {
194
+ return Array.prototype.indexOf.call(this, item);
195
+ },
179
196
  };
180
197
 
181
198
  function LiveNodeList(node,refresh){
@@ -209,7 +226,7 @@ _extends(LiveNodeList,NodeList);
209
226
  * but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
210
227
  * and does not imply that the DOM specifies an order to these Nodes.
211
228
  * NamedNodeMap objects in the DOM are live.
212
- * used for attributes or DocumentType entities
229
+ * used for attributes or DocumentType entities
213
230
  */
214
231
  function NamedNodeMap() {
215
232
  };
@@ -253,7 +270,7 @@ function _removeNamedNode(el,list,attr){
253
270
  }
254
271
  }
255
272
  }else{
256
- throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
273
+ throw new DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
257
274
  }
258
275
  }
259
276
  NamedNodeMap.prototype = {
@@ -298,10 +315,10 @@ NamedNodeMap.prototype = {
298
315
  var attr = this.getNamedItem(key);
299
316
  _removeNamedNode(this._ownerElement,this,attr);
300
317
  return attr;
301
-
302
-
318
+
319
+
303
320
  },// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR
304
-
321
+
305
322
  //for level2
306
323
  removeNamedItemNS:function(namespaceURI,localName){
307
324
  var attr = this.getNamedItemNS(namespaceURI,localName);
@@ -447,10 +464,10 @@ Node.prototype = {
447
464
  prefix : null,
448
465
  localName : null,
449
466
  // Modified in DOM Level 2:
450
- insertBefore:function(newChild, refChild){//raises
467
+ insertBefore:function(newChild, refChild){//raises
451
468
  return _insertBefore(this,newChild,refChild);
452
469
  },
453
- replaceChild:function(newChild, oldChild){//raises
470
+ replaceChild:function(newChild, oldChild){//raises
454
471
  this.insertBefore(newChild,oldChild);
455
472
  if(oldChild){
456
473
  this.removeChild(oldChild);
@@ -656,48 +673,177 @@ function _removeChild (parentNode, child) {
656
673
  _onUpdateChild(parentNode.ownerDocument, parentNode);
657
674
  return child;
658
675
  }
676
+
677
+ /**
678
+ * Returns `true` if `node` can be a parent for insertion.
679
+ * @param {Node} node
680
+ * @returns {boolean}
681
+ */
682
+ function hasValidParentNodeType(node) {
683
+ return (
684
+ node &&
685
+ (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
686
+ );
687
+ }
688
+
689
+ /**
690
+ * Returns `true` if `node` can be inserted according to it's `nodeType`.
691
+ * @param {Node} node
692
+ * @returns {boolean}
693
+ */
694
+ function hasInsertableNodeType(node) {
695
+ return (
696
+ node &&
697
+ (isElementNode(node) ||
698
+ isTextNode(node) ||
699
+ isDocTypeNode(node) ||
700
+ node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
701
+ node.nodeType === Node.COMMENT_NODE ||
702
+ node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
703
+ );
704
+ }
705
+
706
+ /**
707
+ * Returns true if `node` is a DOCTYPE node
708
+ * @param {Node} node
709
+ * @returns {boolean}
710
+ */
711
+ function isDocTypeNode(node) {
712
+ return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
713
+ }
714
+
659
715
  /**
660
- * preformance key(refChild == null)
716
+ * Returns true if the node is an element
717
+ * @param {Node} node
718
+ * @returns {boolean}
661
719
  */
662
- function _insertBefore(parentNode,newChild,nextChild){
663
- var cp = newChild.parentNode;
720
+ function isElementNode(node) {
721
+ return node && node.nodeType === Node.ELEMENT_NODE;
722
+ }
723
+ /**
724
+ * Returns true if `node` is a text node
725
+ * @param {Node} node
726
+ * @returns {boolean}
727
+ */
728
+ function isTextNode(node) {
729
+ return node && node.nodeType === Node.TEXT_NODE;
730
+ }
731
+
732
+ /**
733
+ * Check if en element node can be inserted before `child`, or at the end if child is falsy,
734
+ * according to the presence and position of a doctype node on the same level.
735
+ *
736
+ * @param {Document} doc The document node
737
+ * @param {Node} child the node that would become the nextSibling if the element would be inserted
738
+ * @returns {boolean} `true` if an element can be inserted before child
739
+ * @private
740
+ * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
741
+ */
742
+ function isElementInsertionPossible(doc, child) {
743
+ var parentChildNodes = doc.childNodes || [];
744
+ if (find(parentChildNodes, isElementNode) || isDocTypeNode(child)) {
745
+ return false;
746
+ }
747
+ var docTypeNode = find(parentChildNodes, isDocTypeNode);
748
+ return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
749
+ }
750
+ /**
751
+ * @private
752
+ * @param {Node} parent the parent node to insert `node` into
753
+ * @param {Node} node the node to insert
754
+ * @param {Node=} child the node that should become the `nextSibling` of `node`
755
+ * @returns {Node}
756
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
757
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
758
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
759
+ */
760
+ function _insertBefore(parent, node, child) {
761
+ if (!hasValidParentNodeType(parent)) {
762
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
763
+ }
764
+ if (child && child.parentNode !== parent) {
765
+ throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
766
+ }
767
+ if (
768
+ !hasInsertableNodeType(node) ||
769
+ // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
770
+ // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
771
+ (isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
772
+ ) {
773
+ throw new DOMException(
774
+ HIERARCHY_REQUEST_ERR,
775
+ 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
776
+ );
777
+ }
778
+ var parentChildNodes = parent.childNodes || [];
779
+ var nodeChildNodes = node.childNodes || [];
780
+ if (parent.nodeType === Node.DOCUMENT_NODE) {
781
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
782
+ var nodeChildElements = nodeChildNodes.filter(isElementNode);
783
+ if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
784
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
785
+ }
786
+ if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
787
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
788
+ }
789
+ }
790
+ if (isElementNode(node)) {
791
+ if (find(parentChildNodes, isElementNode) || !isElementInsertionPossible(parent, child)) {
792
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
793
+ }
794
+ }
795
+ if (isDocTypeNode(node)) {
796
+ if (find(parentChildNodes, isDocTypeNode)) {
797
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
798
+ }
799
+ var parentElementChild = find(parentChildNodes, isElementNode);
800
+ if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
801
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
802
+ }
803
+ if (!child && parentElementChild) {
804
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
805
+ }
806
+ }
807
+ }
808
+
809
+ var cp = node.parentNode;
664
810
  if(cp){
665
- cp.removeChild(newChild);//remove and update
811
+ cp.removeChild(node);//remove and update
666
812
  }
667
- if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
668
- var newFirst = newChild.firstChild;
813
+ if(node.nodeType === DOCUMENT_FRAGMENT_NODE){
814
+ var newFirst = node.firstChild;
669
815
  if (newFirst == null) {
670
- return newChild;
816
+ return node;
671
817
  }
672
- var newLast = newChild.lastChild;
818
+ var newLast = node.lastChild;
673
819
  }else{
674
- newFirst = newLast = newChild;
820
+ newFirst = newLast = node;
675
821
  }
676
- var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
822
+ var pre = child ? child.previousSibling : parent.lastChild;
677
823
 
678
824
  newFirst.previousSibling = pre;
679
- newLast.nextSibling = nextChild;
680
-
681
-
825
+ newLast.nextSibling = child;
826
+
827
+
682
828
  if(pre){
683
829
  pre.nextSibling = newFirst;
684
830
  }else{
685
- parentNode.firstChild = newFirst;
831
+ parent.firstChild = newFirst;
686
832
  }
687
- if(nextChild == null){
688
- parentNode.lastChild = newLast;
833
+ if(child == null){
834
+ parent.lastChild = newLast;
689
835
  }else{
690
- nextChild.previousSibling = newLast;
836
+ child.previousSibling = newLast;
691
837
  }
692
838
  do{
693
- newFirst.parentNode = parentNode;
839
+ newFirst.parentNode = parent;
694
840
  }while(newFirst !== newLast && (newFirst= newFirst.nextSibling))
695
- _onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);
696
- //console.log(parentNode.lastChild.nextSibling == null)
697
- if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
698
- newChild.firstChild = newChild.lastChild = null;
841
+ _onUpdateChild(parent.ownerDocument||parent, parent);
842
+ //console.log(parent.lastChild.nextSibling == null)
843
+ if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
844
+ node.firstChild = node.lastChild = null;
699
845
  }
700
- return newChild;
846
+ return node;
701
847
  }
702
848
 
703
849
  /**
@@ -752,11 +898,13 @@ Document.prototype = {
752
898
  }
753
899
  return newChild;
754
900
  }
755
- if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){
901
+ _insertBefore(this, newChild, refChild);
902
+ newChild.ownerDocument = this;
903
+ if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
756
904
  this.documentElement = newChild;
757
905
  }
758
906
 
759
- return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild;
907
+ return newChild;
760
908
  },
761
909
  removeChild : function(oldChild){
762
910
  if(this.documentElement == oldChild){
@@ -950,7 +1098,7 @@ Element.prototype = {
950
1098
  var attr = this.getAttributeNode(name)
951
1099
  attr && this.removeAttributeNode(attr);
952
1100
  },
953
-
1101
+
954
1102
  //four real opeartion method
955
1103
  appendChild:function(newChild){
956
1104
  if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
@@ -974,7 +1122,7 @@ Element.prototype = {
974
1122
  var old = this.getAttributeNodeNS(namespaceURI, localName);
975
1123
  old && this.removeAttributeNode(old);
976
1124
  },
977
-
1125
+
978
1126
  hasAttributeNS : function(namespaceURI, localName){
979
1127
  return this.getAttributeNodeNS(namespaceURI, localName)!=null;
980
1128
  },
@@ -990,7 +1138,7 @@ Element.prototype = {
990
1138
  getAttributeNodeNS : function(namespaceURI, localName){
991
1139
  return this.attributes.getNamedItemNS(namespaceURI, localName);
992
1140
  },
993
-
1141
+
994
1142
  getElementsByTagName : function(tagName){
995
1143
  return new LiveNodeList(this,function(base){
996
1144
  var ls = [];
@@ -1011,7 +1159,7 @@ Element.prototype = {
1011
1159
  }
1012
1160
  });
1013
1161
  return ls;
1014
-
1162
+
1015
1163
  });
1016
1164
  }
1017
1165
  };
@@ -1040,7 +1188,7 @@ CharacterData.prototype = {
1040
1188
  },
1041
1189
  insertData: function(offset,text) {
1042
1190
  this.replaceData(offset,0,text);
1043
-
1191
+
1044
1192
  },
1045
1193
  appendChild:function(newChild){
1046
1194
  throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])
@@ -1134,7 +1282,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
1134
1282
  var refNode = this.nodeType == 9 && this.documentElement || this;
1135
1283
  var prefix = refNode.prefix;
1136
1284
  var uri = refNode.namespaceURI;
1137
-
1285
+
1138
1286
  if(uri && prefix == null){
1139
1287
  //console.log(prefix)
1140
1288
  var prefix = refNode.lookupPrefix(uri);
@@ -1167,8 +1315,8 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
1167
1315
  if (prefix === "xml" && uri === NAMESPACE.XML || uri === NAMESPACE.XMLNS) {
1168
1316
  return false;
1169
1317
  }
1170
-
1171
- var i = visibleNamespaces.length
1318
+
1319
+ var i = visibleNamespaces.length
1172
1320
  while (i--) {
1173
1321
  var ns = visibleNamespaces[i];
1174
1322
  // get namespace prefix
@@ -1219,7 +1367,7 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1219
1367
  var len = attrs.length;
1220
1368
  var child = node.firstChild;
1221
1369
  var nodeName = node.tagName;
1222
-
1370
+
1223
1371
  isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
1224
1372
 
1225
1373
  var prefixedNodeName = nodeName
@@ -1278,14 +1426,14 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1278
1426
  serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
1279
1427
  }
1280
1428
 
1281
- // add namespace for current node
1429
+ // add namespace for current node
1282
1430
  if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
1283
1431
  var prefix = node.prefix||'';
1284
1432
  var uri = node.namespaceURI;
1285
1433
  addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
1286
1434
  visibleNamespaces.push({ prefix: prefix, namespace:uri });
1287
1435
  }
1288
-
1436
+
1289
1437
  if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){
1290
1438
  buf.push('>');
1291
1439
  //if is cdata child node
@@ -1500,7 +1648,7 @@ try{
1500
1648
  }
1501
1649
  }
1502
1650
  })
1503
-
1651
+
1504
1652
  function getTextContent(node){
1505
1653
  switch(node.nodeType){
1506
1654
  case ELEMENT_NODE:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmldom/xmldom",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.",
5
5
  "keywords": [
6
6
  "w3c",
@@ -29,13 +29,15 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "lint": "eslint lib test",
32
+ "format": "prettier --write test",
32
33
  "changelog": "auto-changelog --unreleased-only",
33
34
  "start": "nodemon --watch package.json --watch lib --watch test --exec 'npm --silent run test && npm --silent run lint'",
34
35
  "stryker": "stryker run",
35
36
  "stryker:dry-run": "stryker run -m '' --reporters progress",
36
37
  "test": "jest",
38
+ "testrelease": "npm test && npm run lint",
37
39
  "version": "./changelog-has-version.sh",
38
- "release": "np --no-yarn"
40
+ "release": "np --no-yarn --test-script testrelease"
39
41
  },
40
42
  "engines": {
41
43
  "node": ">=10.0.0"