From 16510789b5f6ad3f49a78a2d8efb6da54c96ed56 Mon Sep 17 00:00:00 2001 From: Kunal Kukreja Date: Sat, 27 Jun 2020 20:22:18 +0530 Subject: [PATCH 1/5] Added namespace validation (default enabled) --- spec/data_spec.js | 4 +- spec/validator_spec.js | 10 ++--- src/validator.js | 89 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/spec/data_spec.js b/spec/data_spec.js index 6cc585f4..a4e97561 100644 --- a/spec/data_spec.js +++ b/spec/data_spec.js @@ -115,7 +115,7 @@ describe("XMLParser", function() { ignoreAttributes: false, //parseAttributeValue: true, allowBooleanAttributes: true - }, true); + }, { ignoreNameSpace: true }); //console.log(JSON.stringify(result,null,4)); expect(result).toEqual(expected); @@ -138,7 +138,7 @@ describe("XMLParser", function() { ignoreAttributes: false, //parseAttributeValue: true, allowBooleanAttributes: true - }, true); + }, { ignoreNameSpace: true }); //console.log(JSON.stringify(result,null,4)); expect(result).toEqual(expected); diff --git a/spec/validator_spec.js b/spec/validator_spec.js index 02f373fb..584fb316 100644 --- a/spec/validator_spec.js +++ b/spec/validator_spec.js @@ -5,7 +5,7 @@ const path = require("path"); const validator = require("../src/validator"); function validate(xmlData, error, line = 1) { - const result = validator.validate(xmlData); + const result = validator.validate(xmlData, { ignoreNameSpace: true }); if (error) { const keys = Object.keys(error); @@ -39,7 +39,7 @@ describe("XMLParser", function () { it("should not validate incomplete xml string", function () { validate("", { - InvalidXml: "Invalid '[ \"rootNode\"]' found." + InvalidXml: "Invalid '[ { \"name\": \"rootNode\" }]' found." }); }); @@ -109,7 +109,7 @@ describe("XMLParser", function () { it("should not validate simple xml string with value but no closing tag", function () { validate("some value", { - InvalidXml: "Invalid '[ \"root:Node\"]' found." + InvalidXml: "Invalid '[ { \"name\": \"root:Node\" }]' found." }); }); @@ -154,13 +154,13 @@ describe("XMLParser", function () { it("should not validate xml with non closing comment", function () { validate("1val"); + validateIgnoringNS("1val"); }); it("should validate xml with comment", function () { - validate("1val"); + validateIgnoringNS("1val"); }); it("should not validate xml with comment in a open tag", function () { - validate(" -- -->>1val", { + validateIgnoringNS(" -- -->>1val", { InvalidTag: "Tag 'rootNode >1val", { + validateIgnoringNS(" -- --> >1val", { InvalidAttr: "boolean attribute '" + "", { + validateIgnoringNS("", { InvalidTag: "Tag '!bla' is an invalid name." }); }); it("should not validate XML when prolog doesn't start from 1st char", function () { - validate(" Hello World.", { + validateIgnoringNS(" Hello World.", { InvalidXml: "XML declaration allowed only at the start of the document." }); }); it("should not validate XML with prolog only", function () { - validate("", { + validateIgnoringNS("", { InvalidXml: "Start tag expected." }); }); it("should not validate XML with prolog & DOCTYPE but not any other tag", function () { - validate("" + + validateIgnoringNS("" + "" + "" + @@ -267,14 +296,14 @@ describe("XMLParser", function () { }); it("should validate XML PIs", function () { - validate('' + + validateIgnoringNS('' + '' + '

' + ''); }); it("should not validate XML PIs with invalid values", function () { - validate('' + + validateIgnoringNS('' + '" ?>' + '

' + '', { @@ -283,17 +312,17 @@ describe("XMLParser", function () { }); it('should validate xml with a "length" attribute', function () { - validate(''); + validateIgnoringNS(''); }); it("should not validate xml with repeated attributes", function () { - validate('', { + validateIgnoringNS('', { InvalidAttr: "Attribute 'length' is repeated." }); }); it('should validate xml with a tag attribute splitted on more lines', () => { - validate(` + validateIgnoringNS(` { - validate(` + validateIgnoringNS(` { - validate(` @@ -323,7 +352,7 @@ attribute2="attribute2" }); it('should detect error line when having multiple attributes 2', () => { - validate(`jekyll & hyde'); - validate('jekyll { hyde'); - validate('jekyll � hyde'); - validate('jekyll h; hyde', error); - validate('jekyll a; hyde', error); - validate('jekyll { hyde', error); - validate('jekyll abcd hyde', error); - validate('jekyll & hyde', error); - validate('jekyll &aa', error); - validate('jekyll &abcdefghij1234567890;'); - validate('jekyll &abcdefghij1234567890a;', error); // limit to 20 chars + validateIgnoringNS('jekyll & hyde'); + validateIgnoringNS('jekyll { hyde'); + validateIgnoringNS('jekyll � hyde'); + validateIgnoringNS('jekyll h; hyde', error); + validateIgnoringNS('jekyll a; hyde', error); + validateIgnoringNS('jekyll { hyde', error); + validateIgnoringNS('jekyll abcd hyde', error); + validateIgnoringNS('jekyll & hyde', error); + validateIgnoringNS('jekyll &aa', error); + validateIgnoringNS('jekyll &abcdefghij1234567890;'); + validateIgnoringNS('jekyll &abcdefghij1234567890a;', error); // limit to 20 chars }); }); describe("should not validate XML documents with multiple root nodes", () => { it('when root nodes are repeated', () => { - validate(``, { + validateIgnoringNS(``, { InvalidXml: 'Multiple possible root nodes found.' }); }); it('when root nodes are different', () => { - validate('', { + validateIgnoringNS('', { InvalidXml: 'Multiple possible root nodes found.' }); }); it('when root nodes have more nested tags', () => { - validate(` + validateIgnoringNS(` From 88c47b3a5e2bd0a08cc0fc05f6ad75384eb2b533 Mon Sep 17 00:00:00 2001 From: Kunal Kukreja Date: Sun, 5 Jul 2020 14:17:09 +0530 Subject: [PATCH 3/5] Added namespace validation for attributes and fix for self closing tags --- spec/validator_spec.js | 77 +++++++++++++++++++++++++++++++++++------- src/validator.js | 66 +++++++++++++++++++++--------------- 2 files changed, 103 insertions(+), 40 deletions(-) diff --git a/spec/validator_spec.js b/spec/validator_spec.js index dd0f15ce..ac1eac4b 100644 --- a/spec/validator_spec.js +++ b/spec/validator_spec.js @@ -4,8 +4,8 @@ const fs = require("fs"); const path = require("path"); const validator = require("../src/validator"); -function validate(xmlData, ignoreNameSpace, error, line = 1) { - const result = validator.validate(xmlData, { ignoreNameSpace: ignoreNameSpace }); +function validate(xmlData, options, error, line = 1) { + const result = validator.validate(xmlData, options); if (error) { const keys = Object.keys(error); @@ -21,11 +21,11 @@ function validate(xmlData, ignoreNameSpace, error, line = 1) { } function validateIgnoringNS(xmlData, error, line) { - validate(xmlData, true, error, line); + validate(xmlData, { ignoreNameSpace: true }, error, line); } function validateWithNS(xmlData, error, line) { - validate(xmlData, false, error, line); + validate(xmlData, null, error, line); } function validateFile(fileName, ...args) { @@ -94,29 +94,82 @@ describe("XMLParser", function () { InvalidTag: "Closing tag 'rootnode' can't have attributes or invalid starting." }); }); - - it("should validate simple xml string with namespace", function () { + + it("should validate tag with namespace", function () { validateWithNS(""); }); - it("should not validate simple xml string when namespace is not defined", function () { + it("should validate attribute with namespace", function () { + validateWithNS(""); + }); + + it("should validate self closing tag with namespace", function () { + validateWithNS(""); + }); + + it("should validate attributes in self closing tag with namespace", function () { + validateWithNS(""); + }); + + it("should not validate other tags with namespace when namespace is defined in self closing tag", function () { + validateWithNS("", { + InvalidTag: "Namespace prefix 'ns' is not defined for 'ns:testTag'" + }); + }); + + it("should not validate attributes outside self closing tag with namespace definition", function () { + validateWithNS("", { + InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'" + }); + }); + + it("should not validate tags with namespace when namespace is defined in a sibling tag", function () { + validateWithNS("", { + InvalidTag: "Namespace prefix 'ns' is not defined for 'ns:child2'" + }); + }); + + it("should not validate tag when namespace is not defined", function () { validateWithNS("", { - InvalidNS: "Namespace prefix 'root' is not defined for tag 'root:Node'" + InvalidTag: "Namespace prefix 'root' is not defined for 'root:Node'" + }); + }); + + it("should not validate attribute when namespace is not defined", function () { + validateWithNS("", { + InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'" }); }); - it("should not validate xml when namespace is defined later", function () { + it("should not validate tag when namespace is defined later", function () { validateWithNS(` `, { - InvalidNS: "Namespace prefix 'root' is not defined for tag 'root:Node'" + InvalidTag: "Namespace prefix 'root' is not defined for 'root:Node'" }); }); - it("should not validate simple xml string when multiple namespace prefixes are present", function () { + it("should not validate attribute when namespace is defined later", function () { + validateWithNS(` + + + + + `, { + InvalidAttr: "Namespace prefix 'ns' is not defined for 'ns:attr'" + }, 2); + }); + + it("should not validate tag when multiple namespace prefixes are present", function () { validateWithNS("", { - InvalidNS: "Tag 'root:ns:Node' cannot have multiple namespace prefixes" + InvalidTag: "'root:ns:Node' cannot have multiple namespace prefixes" + }); + }); + + it("should not validate attribute when multiple namespace prefixes are present", function () { + validateWithNS("", { + InvalidAttr: "'ns1:ns2:attr' cannot have multiple namespace prefixes" }); }); diff --git a/src/validator.js b/src/validator.js index 7ad3b488..38a554f0 100644 --- a/src/validator.js +++ b/src/validator.js @@ -94,8 +94,8 @@ exports.validate = function (xmlData, options) { const nsResult = validateNameSpace(tagName, nameSpaces); - if (!nsResult.isValid) { - return getErrorObject('InvalidNS', nsResult.errorMsg, getLineNumberForPosition(xmlData, i)); + if (nsResult !== true) { + return getErrorObject('InvalidTag', nsResult, getLineNumberForPosition(xmlData, i)); } } @@ -105,9 +105,16 @@ exports.validate = function (xmlData, options) { if (attrStr[attrStr.length - 1] === '/') { //self closing tag attrStr = attrStr.substring(0, attrStr.length - 1); - const isValid = validateAttributeString(attrStr, options); + const isValid = validateAttributeString(attrStr, nameSpaces, options); if (isValid === true) { tagFound = true; + + if (!options.ignoreNameSpace && result.nsArray.length > 0) { + //Popping namespaces defined in tag + for (let x=0; x < result.nsArray.length; x++) { + nameSpaces.pop(result.nsArray[x]); + } + } //continue; //text may presents after self closing tag } else { //the result from the nested function returns the position of the error within the attribute @@ -127,7 +134,7 @@ exports.validate = function (xmlData, options) { } if (!options.ignoreNameSpace && otg.nsArray.length > 0) { - //Pushing namespaces defined in tag + //Popping namespaces defined in tag for (let x=0; x < otg.nsArray.length; x++) { nameSpaces.pop(otg.nsArray[x]); } @@ -139,7 +146,7 @@ exports.validate = function (xmlData, options) { } } } else { - const isValid = validateAttributeString(attrStr, options); + const isValid = validateAttributeString(attrStr, nameSpaces, options); if (isValid !== true) { //the result from the nested function returns the position of the error within the attribute //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute @@ -334,7 +341,7 @@ const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s //attr, ="sd", a="amit's", a="sd"b="saf", ab cd="" -function validateAttributeString(attrStr, options) { +function validateAttributeString(attrStr, nsArray, options) { //console.log("start:"+attrStr+":end"); //if(attrStr.trim().length === 0) return true; //empty string @@ -357,6 +364,18 @@ function validateAttributeString(attrStr, options) { if (!validateAttrName(attrName)) { return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(attrStr, matches[i][0])); } + + if (!options.ignoreNameSpace) { + const nsDefMatches = util.getAllMatches(matches[i][0], nameSpaceDefinitionRegex); + //Skipping namespace definition attribute + if (!nsDefMatches || nsDefMatches.length === 0 || attrName !== nsDefMatches[0][1]) { + const nsResult = validateNameSpace(attrName, nsArray); + if (nsResult !== true) { + return getErrorObject('InvalidAttr', nsResult, getPositionFromMatch(attrStr, matches[i][0])); + } + } + } + if (!attrNames.hasOwnProperty(attrName)) { //check for duplicate attribute. attrNames[attrName] = 1; @@ -423,38 +442,29 @@ function validateTagName(tagname) { return util.isName(tagname) /* && !tagname.match(startsWithXML) */; } -function validateNameSpace(tagName, nsArray) { - let tagSplit = tagName.split(":"); - let isValid, errorMsg; - switch (tagSplit.length){ +const nameSpaceDefinitionRegex = new RegExp(/(xmlns:([^:=]+))=/, 'g'); + +function validateNameSpace(elemName, nsArray) { + let elemSplit = elemName.split(":"); + switch (elemSplit.length){ case 1: - isValid = true; - break; + return true; case 2: - if (nsArray.indexOf(tagSplit[0]) > -1) { - isValid = true; + if (nsArray.indexOf(elemSplit[0]) > -1) { + return true; } else { - isValid = false; - errorMsg = "Namespace prefix '" + tagSplit[0] + "' is not defined for tag '" + tagName + "'"; + return "Namespace prefix '" + elemSplit[0] + "' is not defined for '" + elemName + "'"; } - break; default: - isValid = false; - errorMsg = "Tag '" + tagName + "' cannot have multiple namespace prefixes"; - } - return { - isValid: isValid, - errorMsg: errorMsg + return "'" + elemName + "' cannot have multiple namespace prefixes"; } } function getNameSpaceDefinitions(attributeString) { - const regexNs = /xmlns:([^=]+)=/g; let nsArray = []; - let matches = regexNs.exec(attributeString); - while (matches){ - nsArray.push(matches[1]); - matches = regexNs.exec(attributeString); + let matches = util.getAllMatches(attributeString, nameSpaceDefinitionRegex); + for (let i=0; i< matches.length; i++){ + nsArray.push(matches[i][2]); } return nsArray; } From d5e4ea0ba017eaefba0131c4bb6c2f485759b1ed Mon Sep 17 00:00:00 2001 From: Kunal Kukreja Date: Sun, 5 Jul 2020 18:22:55 +0530 Subject: [PATCH 4/5] Changed approach for adding and removing namespaces --- src/validator.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/validator.js b/src/validator.js index 38a554f0..dbdc80db 100644 --- a/src/validator.js +++ b/src/validator.js @@ -85,12 +85,8 @@ exports.validate = function (xmlData, options) { } if (!options.ignoreNameSpace) { - if (result.nsArray.length > 0) { - //Pushing namespaces defined in tag - for (let x=0; x < result.nsArray.length; x++) { - nameSpaces.push(result.nsArray[x]); - } - } + //Pushing namespaces defined in tag + Array.prototype.push.apply(nameSpaces, result.nsArray); const nsResult = validateNameSpace(tagName, nameSpaces); @@ -109,11 +105,9 @@ exports.validate = function (xmlData, options) { if (isValid === true) { tagFound = true; - if (!options.ignoreNameSpace && result.nsArray.length > 0) { - //Popping namespaces defined in tag - for (let x=0; x < result.nsArray.length; x++) { - nameSpaces.pop(result.nsArray[x]); - } + if (!options.ignoreNameSpace) { + //Removing namespaces defined in current tag + nameSpaces.length -= result.nsArray.length; } //continue; //text may presents after self closing tag } else { @@ -133,11 +127,9 @@ exports.validate = function (xmlData, options) { return getErrorObject('InvalidTag', "Closing tag '"+otg.name+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i)); } - if (!options.ignoreNameSpace && otg.nsArray.length > 0) { - //Popping namespaces defined in tag - for (let x=0; x < otg.nsArray.length; x++) { - nameSpaces.pop(otg.nsArray[x]); - } + if (!options.ignoreNameSpace) { + //Removing namespaces defined in current tag + nameSpaces.length -= otg.nsArray.length; } //when there are no more tags, we reached the root level. From ca8ffd58a17f05f8a1760d0c84d91af16429e3db Mon Sep 17 00:00:00 2001 From: Kunal Kukreja Date: Sat, 11 Jul 2020 13:52:40 +0530 Subject: [PATCH 5/5] 1. Added support for validating duplicate attributes with namespace 2. Added check for empty namespace URI --- spec/validator_spec.js | 38 +++++++++++++++++++++++++++++++- src/validator.js | 50 ++++++++++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/spec/validator_spec.js b/spec/validator_spec.js index ac1eac4b..2daf0551 100644 --- a/spec/validator_spec.js +++ b/spec/validator_spec.js @@ -103,6 +103,20 @@ describe("XMLParser", function () { validateWithNS(""); }); + it("should not validate namespace attribute with empty URI", function () { + validateWithNS("", { + InvalidAttr: "Invalid URI for namespace root" + }); + }); + + it("should validate all namespaces defined in a tag", function () { + validateWithNS(` + + + + `); + }); + it("should validate self closing tag with namespace", function () { validateWithNS(""); }); @@ -168,11 +182,27 @@ describe("XMLParser", function () { }); it("should not validate attribute when multiple namespace prefixes are present", function () { - validateWithNS("", { + validateWithNS("", { InvalidAttr: "'ns1:ns2:attr' cannot have multiple namespace prefixes" }); }); + it("should not validate attributes with same name and same namespace prefix", function () { + validateWithNS("", { + InvalidAttr: "Attribute 'attr' in namespace 'urn:none' is repeated." + }); + }); + + it("should not validate attributes with same name and same namespace", function () { + validateWithNS("",{ + InvalidAttr: "Attribute 'attr' in namespace 'urn:none' is repeated." + }); + }); + + it("should validate attributes with same name and different namespace", function () { + validateWithNS(""); + }); + it("should not validate xml string with namespace when closing tag is diffrent", function () { validateIgnoringNS("", { InvalidTag: "Closing tag 'root:Node' is expected inplace of 'root:node'." @@ -374,6 +404,12 @@ describe("XMLParser", function () { }); }); + it("should not validate attributes with same name and different namespace prefix, if namespace is ignored", function () { + validateIgnoringNS("", { + InvalidAttr: "Attribute 'attr' is repeated." + }); + }); + it('should validate xml with a tag attribute splitted on more lines', () => { validateIgnoringNS(` 1) { + for (let i=0; i < nsArray.length; i++) { + const nsSplit = nsArray[i].split("'"); + if (nsSplit[0] === attrSplit[0]) { + attrName = nsSplit[1] + "'" + attrSplit[1]; + break; + } + } + } + } else { + attrName = attrName.replace(/[^:]+:/, ""); } if (!attrNames.hasOwnProperty(attrName)) { - //check for duplicate attribute. - attrNames[attrName] = 1; + //check for duplicate attribute. + attrNames[attrName] = 1; } else { - return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(attrStr, matches[i][0])); + const attrSplit = attrName.split("'"); + const msg = attrSplit.length === 2 ? "'" + attrSplit[1] + "' in namespace '" + attrSplit[0] + "'": "'" + attrName + "'"; + return getErrorObject('InvalidAttr', "Attribute "+ msg +" is repeated.", getPositionFromMatch(attrStr, matches[i][0])); } } @@ -434,7 +457,7 @@ function validateTagName(tagname) { return util.isName(tagname) /* && !tagname.match(startsWithXML) */; } -const nameSpaceDefinitionRegex = new RegExp(/(xmlns:([^:=]+))=/, 'g'); +const nameSpaceDefinitionRegex = new RegExp(/(xmlns:([^:=]+))=['"]([^'"]*)['"]/, 'g'); function validateNameSpace(elemName, nsArray) { let elemSplit = elemName.split(":"); @@ -442,11 +465,12 @@ function validateNameSpace(elemName, nsArray) { case 1: return true; case 2: - if (nsArray.indexOf(elemSplit[0]) > -1) { - return true; - } else { - return "Namespace prefix '" + elemSplit[0] + "' is not defined for '" + elemName + "'"; + for (let i=0; i< nsArray.length; i++) { + if (nsArray[i].split("'")[0] === elemSplit[0]){ + return true; + } } + return "Namespace prefix '" + elemSplit[0] + "' is not defined for '" + elemName + "'"; default: return "'" + elemName + "' cannot have multiple namespace prefixes"; } @@ -456,7 +480,11 @@ function getNameSpaceDefinitions(attributeString) { let nsArray = []; let matches = util.getAllMatches(attributeString, nameSpaceDefinitionRegex); for (let i=0; i< matches.length; i++){ - nsArray.push(matches[i][2]); + if (matches[i][3]) { + nsArray.push(matches[i][2] + "'" + matches[i][3]); + } else { + return "Invalid URI for namespace " + matches[i][2]; + } } return nsArray; }