diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 885d43d..0ecdd5a 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -239,11 +239,11 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR } /** - * Normalize and apply context-levevl @protected terms onto each term separately. + * Normalize and apply context-level @protected terms onto each term separately. * @param {IJsonLdContextNormalizedRaw} context A context. * @param {number} processingMode The processing mode. */ - public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions) { + public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions, expandOptions: IExpandOptions) { if (processingMode && processingMode >= 1.1) { if (context['@protected']) { for (const key of Object.keys(context)) { @@ -264,6 +264,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR '@id': value, '@protected': true, }; + if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { + context[key]['@prefix'] = true + } } } } @@ -287,16 +290,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - const isPrefix = Util.isSimpleTermDefinitionPrefix(contextAfter[key], expandOptions); contextAfter[key] = { '@id': contextAfter[key] }; - - // If the simple term def was a prefix, explicitly mark the term as a prefix in the expanded term definition, - // because otherwise we loose this information due to JSON-LD interpreting prefixes differently - // in simple vs expanded term definitions. - if (isPrefix) { - contextAfter[key]['@prefix'] = true; - contextBefore[key]['@prefix'] = true; // Also on before, to make sure the next step still considers them == - } } // Convert term values to strings for each comparison @@ -729,9 +723,6 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP parentContext = JSON.parse(JSON.stringify(parentContext)); } - // We have an actual context object. - let newContext: IJsonLdContextNormalizedRaw = {}; - // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { delete context['@base']; @@ -768,13 +759,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } } - // Merge different parts of the final context in order - newContext = { - ...newContext, - ...(typeof parentContext === 'object' ? parentContext : {}), - ...importContext, - ...context, - }; + this.applyScopedProtected(importContext, { processingMode }, defaultExpandOptions); + let newContext: IJsonLdContextNormalizedRaw = { ...importContext, ...context }; + if (typeof parentContext === 'object') { + // Merge different parts of the final context in order + this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); + newContext = { ...parentContext, ...newContext }; + } + const newContextWrapped = new JsonLdContextNormalized(newContext); // Parse inner contexts with minimal processing @@ -803,7 +795,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } this.normalize(newContext, { processingMode, normalizeLanguageTags }); - this.applyScopedProtected(newContext, { processingMode }); + this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); if (this.validateContext) { this.validate(newContext, { processingMode }); } diff --git a/package.json b/package.json index 0dc08c0..c5510f7 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "test": "jest ${1}", "test-watch": "jest ${1} --watch", "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls", - "lint": "tslint index.ts lib/**/*.ts test/**/*.ts --exclude '**/*.d.ts'", + "lint": "tslint index.ts lib/**/*.ts test/**/*.ts test/*.ts --exclude '**/*.d.ts'", "build": "tsc", "build-watch": "tsc --watch", "validate": "npm ls", diff --git a/test/ContextParser-test.ts b/test/ContextParser-test.ts index 4b05144..5e47020 100644 --- a/test/ContextParser-test.ts +++ b/test/ContextParser-test.ts @@ -2132,6 +2132,210 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); })); }); + it('should parse 2 contexts where one is protected', () => { + return expect(parser.parse([ + { + "@version": 1.0, + "ex":"https://example.org/ns/" + }, + { + "@version": 1.1, + "@protected": true, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + } + ])).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + }, + ex: "https://example.org/ns/" + })); + }); + + const contexts = [ + { + "@version": 1.1, + "@protected": true, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + }, + { + "ex":"https://example.org/ns/", + "@version": 1.1, + "@protected": false, + } + ]; + + const result = new JsonLdContextNormalized({ + "@version": 1.1, + "@protected": false, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + }, + ex: "https://example.org/ns/" + }); + + it('should parse 2 contexts where one is protected and one globally has protected false', () => { + return expect(parser.parse(contexts)).resolves.toEqual(result); + }); + + it('should parse 2 contexts where one is protected and one globally has protected false [reverse order]', () => { + return expect(parser.parse([contexts[1], contexts[0]])).resolves.toEqual(result); + }); + + const BASIC_VC_CONTEXT = { + "@version": 1.1, + "@protected": true, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + } + + it('protected context should override the unprotected context', () => { + return expect(parser.parse([ + { + "@version": 1.0, + "VerifiableCredential":"https://example.org/ns/" + }, + BASIC_VC_CONTEXT + ])).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + } + })); + }); + + describe('with imported contexts', () => { + beforeEach(() => { + jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => { + return new Response(JSON.stringify({ + "@context": { + "MyType": { + "@id": "http://example.org#MyType", + } + } + }), { headers: new Headers([ + ['content-type', 'application/ld+json'] + ]) }) + }); + }); + + it('protected should be applied to the imported context', () => { + return expect(parser.parse([ + { + ...BASIC_VC_CONTEXT, + "@import": "http://example.org/imported/context" + } + ])).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + }, + MyType: { + "@id": "http://example.org#MyType", + "@protected": true, + } + })); + }); + + it.each([ + ['child', [BASIC_VC_CONTEXT, {"@import": "http://example.org/imported/context"}]], + ['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT]], + ])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => { + return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + }, + MyType: { "@id": "http://example.org#MyType" } + })); + }); + }); + + + describe('with imported contexts containing the @protected keyword', () => { + + const BASIC_VC_CONTEXT_WITHOUT_PROTECTED = { + "@version": 1.1, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + } + + beforeEach(() => { + jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => { + return new Response(JSON.stringify({ + "@context": { + "@protected": true, + "MyType": "http://example.org#MyType" + } + }), { headers: new Headers([ + ['content-type', 'application/ld+json'] + ]) }) + }); + }); + + it('protected should be applied to the imported context', () => { + return expect(parser.parse([ + { + ...BASIC_VC_CONTEXT_WITHOUT_PROTECTED, + "@import": "http://example.org/imported/context" + } + ])).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + }, + MyType: { + "@id": "http://example.org#MyType", + "@protected": true, + } + })); + }); + + it.each([ + ['child', [BASIC_VC_CONTEXT_WITHOUT_PROTECTED, {"@import": "http://example.org/imported/context"}]], + ['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT_WITHOUT_PROTECTED]], + ])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => { + return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + }, + MyType: { "@id": "http://example.org#MyType", "@protected": true } + })); + }); + }); + + it('should parse 2 contexts where one is protected', () => { + return expect(parser.parse([ + {"ex":"https://example.org/ns/"}, + { + "@version": 1.1, + "@protected": true, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + } + ])).resolves.toEqual(new JsonLdContextNormalized({ + "@version": 1.1, + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@protected": true, + }, + ex: "https://example.org/ns/" + })); + }); + it('should parse a single keyword alias', () => { return expect(parser.parse({ id: { @@ -2214,6 +2418,60 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); ERROR_CODES.PROTECTED_TERM_REDEFINITION)); }); + it('should error on a protected term with override when the overriding version is 1.0', () => { + return expect(parser.parse([ + { + name: { + '@id': 'http://xmlns.com/foaf/0.1/name', + '@protected': true, + }, + }, + { + "@version": 1.0, + name: 'http://schema.org/name', + }, + ])).rejects.toThrow(new ErrorCoded( + 'Attempted to override the protected keyword name from ' + + '"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"', + ERROR_CODES.PROTECTED_TERM_REDEFINITION)); + }); + + it('should error on an outer scope protected term with override of another protected term', () => { + return expect(parser.parse([ + { + "VerifiableCredential":"https://example.org/ns/", + "@protected": true + }, + { + "@version": 1.1, + "@protected": true, + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + } + } + ], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded( + 'Attempted to override the protected keyword VerifiableCredential from ' + + '"https://example.org/ns/" to "https://www.w3.org/2018/credentials#VerifiableCredential"', + ERROR_CODES.PROTECTED_TERM_REDEFINITION)); + }); + + it('should error on an outer scope protected term with override', () => { + return expect(parser.parse([ + { + '@protected': true, + name: { + '@id': 'http://xmlns.com/foaf/0.1/name', + }, + }, + { + name: 'http://schema.org/name', + }, + ], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded( + 'Attempted to override the protected keyword name from ' + + '"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"', + ERROR_CODES.PROTECTED_TERM_REDEFINITION)); + }); + it('should not error on a protected term with override if ignoreProtection is true', () => { return expect(parser.parse([ { @@ -2482,6 +2740,21 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); })); }); + it('should parse a globally protected string term ending in gen-delim', () => { + return expect(parser.parse([ + { + '@protected': true, + 'foo': 'http://example/foo#', + } + ], { processingMode: 1.1 })).resolves.toEqual(new JsonLdContextNormalized({ + foo: { + '@id': 'http://example/foo#', + '@prefix': true, + '@protected': true, + }, + })); + }); + it('should parse a globally protected string term ending in non-gen-delim with identical override', () => { return expect(parser.parse([ { @@ -2623,6 +2896,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); "@base": "http://base.org/", "ex": { "@id": "http://ex.org/", + "@prefix": true, "@protected": true }, "foo": {