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..2daf0551 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, error, line = 1) { - const result = validator.validate(xmlData); +function validate(xmlData, options, error, line = 1) { + const result = validator.validate(xmlData, options); if (error) { const keys = Object.keys(error); @@ -20,183 +20,295 @@ function validate(xmlData, error, line = 1) { } } +function validateIgnoringNS(xmlData, error, line) { + validate(xmlData, { ignoreNameSpace: true }, error, line); +} + +function validateWithNS(xmlData, error, line) { + validate(xmlData, null, error, line); +} + function validateFile(fileName, ...args) { const fileNamePath = path.join(__dirname, "assets/" + fileName); - validate(fs.readFileSync(fileNamePath).toString(), ...args); + validateIgnoringNS(fs.readFileSync(fileNamePath).toString(), ...args); } describe("XMLParser", function () { it("should validate simple xml string", function () { - validate(""); - validate(``); + validateIgnoringNS(""); + validateIgnoringNS(``); }); it("should not validate invalid starting tag", function () { - validate("< rootNode>", { + validateIgnoringNS("< rootNode>", { InvalidTag: "There is an unnecessary space between tag name and backward slash '", { - InvalidXml: "Invalid '[ \"rootNode\"]' found." + validateIgnoringNS("", { + InvalidXml: "Invalid '[ { \"name\": \"rootNode\" }]' found." }); }); it("should not validate invalid starting tag for following characters", function () { - validate("", { + validateIgnoringNS("", { InvalidTag: "Tag 'rootNode#@aa' is an invalid name." }); }); it("should return false for non xml text", function () { - validate("rootNode", { + validateIgnoringNS("rootNode", { InvalidChar: "char 'r' is not expected." }); }); it("should validate self closing tags", function () { - validate("texttext"); + validateIgnoringNS("texttext"); }); it("should not consider these as self closing tags", function () { - validate("", { + validateIgnoringNS("", { InvalidAttr: "boolean attribute 'tag' is not allowed." }); - validate("", { + validateIgnoringNS("", { InvalidAttr: "Attribute '/' has no space in starting." }); }); it("should not validate xml string when closing tag is different", function () { - validate("", { + validateIgnoringNS("", { InvalidTag: "Closing tag 'rootNode' is expected inplace of 'rootnode'." }); }); it("should not validate xml string when closing tag is invalid", function () { - validate("< /rootnode>", { + validateIgnoringNS("< /rootnode>", { InvalidTag: "There is an unnecessary space between tag name and backward slash '", { + validateIgnoringNS("", { InvalidTag: "There is an unnecessary space between tag name and backward slash '", { + validateIgnoringNS("", { InvalidTag: "Closing tag 'rootnode' can't have attributes or invalid starting." }); }); + + it("should validate tag with namespace", function () { + validateWithNS(""); + }); + + it("should validate attribute with namespace", 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(""); + }); + + 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 validate simple xml string with namespace", function () { - validate(""); + 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("", { + 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 tag when namespace is defined later", function () { + validateWithNS(` + + + `, { + InvalidTag: "Namespace prefix 'root' is not defined for 'root:Node'" + }); + }); + + 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("", { + 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" + }); + }); + + 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 () { - validate("", { + validateIgnoringNS("", { InvalidTag: "Closing tag 'root:Node' is expected inplace of 'root:node'." }); }); it("should validate simple xml string with value", function () { - validate("some value"); + validateIgnoringNS("some value"); }); it("should not validate simple xml string with value but not matching closing tag", function () { - validate("some value", { + validateIgnoringNS("some value", { InvalidTag: "Closing tag 'root:Node' is expected inplace of 'root'." }); }); it("should not validate simple xml string with value but no closing tag", function () { - validate("some value", { - InvalidXml: "Invalid '[ \"root:Node\"]' found." + validateIgnoringNS("some value", { + InvalidXml: "Invalid '[ { \"name\": \"root:Node\" }]' found." }); }); it("should validate xml with nested tags", function () { - validate("1val"); + validateIgnoringNS("1val"); }); it("should not validate xml with wrongly nested tags", function () { - validate("1val", { + validateIgnoringNS("1val", { InvalidTag: "Closing tag 'tag1' is expected inplace of 'tag'." }); }); it("should validate xml with 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 +379,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 +395,23 @@ 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 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', () => { - validate(` + validateIgnoringNS(` { - validate(` + validateIgnoringNS(` { - validate(` @@ -323,7 +441,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(` diff --git a/src/validator.js b/src/validator.js index 783771c3..f4c94b64 100644 --- a/src/validator.js +++ b/src/validator.js @@ -4,9 +4,10 @@ const util = require('./util'); const defaultOptions = { allowBooleanAttributes: false, //A tag can have attributes without any value + ignoreNameSpace: false //Ignore namespace verification }; -const props = ['allowBooleanAttributes']; +const props = ['allowBooleanAttributes', 'ignoreNameSpace']; //const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g"); exports.validate = function (xmlData, options) { @@ -16,6 +17,7 @@ exports.validate = function (xmlData, options) { //xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag //xmlData = xmlData.replace(/()/g,"");//Remove DOCTYPE const tags = []; + let nameSpaces = []; let tagFound = false; //indicates that the root tag has been closed (aka. depth 0 has been reached) @@ -77,19 +79,40 @@ exports.validate = function (xmlData, options) { return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i)); } - const result = readAttributeStr(xmlData, i); + const result = readAttributeStr(xmlData, i, options); if (result === false) { return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i)); } + + if (!options.ignoreNameSpace) { + if (result.nsError) { + return getErrorObject('InvalidAttr', result.nsError, getLineNumberForPosition(xmlData, i)); + } + + //Pushing namespaces defined in tag + Array.prototype.push.apply(nameSpaces, result.nsArray); + + const nsResult = validateNameSpace(tagName, nameSpaces); + + if (nsResult !== true) { + return getErrorObject('InvalidTag', nsResult, getLineNumberForPosition(xmlData, i)); + } + } + let attrStr = result.value; i = result.index; 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) { + //Removing namespaces defined in current tag + nameSpaces.length -= result.nsArray.length; + } //continue; //text may presents after self closing tag } else { //the result from the nested function returns the position of the error within the attribute @@ -104,8 +127,13 @@ exports.validate = function (xmlData, options) { return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, i)); } else { const otg = tags.pop(); - if (tagName !== otg) { - return getErrorObject('InvalidTag', "Closing tag '"+otg+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i)); + if (tagName !== otg.name) { + return getErrorObject('InvalidTag', "Closing tag '"+otg.name+"' is expected inplace of '"+tagName+"'.", getLineNumberForPosition(xmlData, i)); + } + + 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. @@ -114,7 +142,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 @@ -126,7 +154,15 @@ exports.validate = function (xmlData, options) { if (reachedRoot === true) { return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i)); } else { - tags.push(tagName); + let tagObject = { + name: tagName + }; + + if (!options.ignoreNameSpace) { + tagObject["nsArray"] = result.nsArray; + } + + tags.push(tagObject); } tagFound = true; } @@ -255,7 +291,7 @@ var singleQuote = "'"; * @param {string} xmlData * @param {number} i */ -function readAttributeStr(xmlData, i) { +function readAttributeStr(xmlData, i, options) { let attrStr = ''; let startChar = ''; let tagClosed = false; @@ -281,11 +317,22 @@ function readAttributeStr(xmlData, i) { return false; } - return { + let result = { value: attrStr, index: i, tagClosed: tagClosed }; + + if (!options.ignoreNameSpace) { + const nsResult = getNameSpaceDefinitions(attrStr); + if (Array.isArray(nsResult)) { + result["nsArray"] = nsResult; + } else { + result["nsError"] = nsResult; + } + } + + return result; } /** @@ -295,7 +342,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 @@ -314,15 +361,41 @@ function validateAttributeString(attrStr, options) { /* else if(matches[i][6] === undefined){//attribute without value: ab= return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}}; } */ - const attrName = matches[i][2]; + let attrName = matches[i][2]; 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])); + } + } + const attrSplit = attrName.split(":"); + if (attrSplit.length > 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])); } } @@ -384,6 +457,38 @@ function validateTagName(tagname) { return util.isName(tagname) /* && !tagname.match(startsWithXML) */; } +const nameSpaceDefinitionRegex = new RegExp(/(xmlns:([^:=]+))=['"]([^'"]*)['"]/, 'g'); + +function validateNameSpace(elemName, nsArray) { + let elemSplit = elemName.split(":"); + switch (elemSplit.length){ + case 1: + return true; + case 2: + 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"; + } +} + +function getNameSpaceDefinitions(attributeString) { + let nsArray = []; + let matches = util.getAllMatches(attributeString, nameSpaceDefinitionRegex); + for (let i=0; i< matches.length; i++){ + if (matches[i][3]) { + nsArray.push(matches[i][2] + "'" + matches[i][3]); + } else { + return "Invalid URI for namespace " + matches[i][2]; + } + } + return nsArray; +} + //this function returns the line number for the character at the given index function getLineNumberForPosition(xmlData, index) { var lines = xmlData.substring(0, index).split(/\r?\n/);