diff --git a/src/index.ts b/src/index.ts index d9fa01c92..6b6f5de45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -216,8 +216,10 @@ export * from './util/handlers/UnsupportedAsyncHandler'; export * from './util/handlers/WaterfallHandler'; // Util/Identifiers +export * from './util/identifiers/BaseIdentifierStrategy'; export * from './util/identifiers/IdentifierStrategy'; export * from './util/identifiers/SingleRootIdentifierStrategy'; +export * from './util/identifiers/SubdomainIdentifierStrategy'; // Util/Locking export * from './util/locking/ExpiringReadWriteLocker'; diff --git a/src/util/identifiers/BaseIdentifierStrategy.ts b/src/util/identifiers/BaseIdentifierStrategy.ts new file mode 100644 index 000000000..d87e989f5 --- /dev/null +++ b/src/util/identifiers/BaseIdentifierStrategy.ts @@ -0,0 +1,29 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import { InternalServerError } from '../errors/InternalServerError'; +import { ensureTrailingSlash } from '../PathUtil'; +import type { IdentifierStrategy } from './IdentifierStrategy'; + +/** + * Provides a default implementation for `getParentContainer` + * which checks if the identifier is supported and not a root container. + * If not, the last part before the first relevant slash will be removed to find the parent. + */ +export abstract class BaseIdentifierStrategy implements IdentifierStrategy { + public abstract supportsIdentifier(identifier: ResourceIdentifier): boolean; + + public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier { + if (!this.supportsIdentifier(identifier)) { + throw new InternalServerError(`${identifier.path} is not supported`); + } + if (this.isRootContainer(identifier)) { + throw new InternalServerError(`${identifier.path} is a root container and has no parent`); + } + + // Trailing slash is necessary for URL library + const parentPath = new URL('..', ensureTrailingSlash(identifier.path)).href; + + return { path: parentPath }; + } + + public abstract isRootContainer(identifier: ResourceIdentifier): boolean; +} diff --git a/src/util/identifiers/SingleRootIdentifierStrategy.ts b/src/util/identifiers/SingleRootIdentifierStrategy.ts index d5625cd07..da4b01458 100644 --- a/src/util/identifiers/SingleRootIdentifierStrategy.ts +++ b/src/util/identifiers/SingleRootIdentifierStrategy.ts @@ -1,17 +1,17 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../../logging/LogUtil'; -import { InternalServerError } from '../errors/InternalServerError'; import { ensureTrailingSlash } from '../PathUtil'; -import type { IdentifierStrategy } from './IdentifierStrategy'; +import { BaseIdentifierStrategy } from './BaseIdentifierStrategy'; /** * An IdentifierStrategy that assumes there is only 1 root and all other identifiers are made by appending to that root. */ -export class SingleRootIdentifierStrategy implements IdentifierStrategy { +export class SingleRootIdentifierStrategy extends BaseIdentifierStrategy { private readonly baseUrl: string; protected readonly logger = getLoggerFor(this); public constructor(baseUrl: string) { + super(); this.baseUrl = ensureTrailingSlash(baseUrl); } @@ -23,20 +23,6 @@ export class SingleRootIdentifierStrategy implements IdentifierStrategy { return supported; } - public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier { - if (!this.supportsIdentifier(identifier)) { - throw new InternalServerError(`${identifier.path} is not supported`); - } - if (this.isRootContainer(identifier)) { - throw new InternalServerError(`${identifier.path} is a root container and has no parent`); - } - - // Trailing slash is necessary for URL library - const parentPath = new URL('..', ensureTrailingSlash(identifier.path)).href; - - return { path: parentPath }; - } - public isRootContainer(identifier: ResourceIdentifier): boolean { return identifier.path === this.baseUrl; } diff --git a/src/util/identifiers/SubdomainIdentifierStrategy.ts b/src/util/identifiers/SubdomainIdentifierStrategy.ts new file mode 100644 index 000000000..11d9a7944 --- /dev/null +++ b/src/util/identifiers/SubdomainIdentifierStrategy.ts @@ -0,0 +1,32 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import { getLoggerFor } from '../../logging/LogUtil'; +import { ensureTrailingSlash, createSubdomainRegexp } from '../PathUtil'; +import { BaseIdentifierStrategy } from './BaseIdentifierStrategy'; + +/** + * An IdentifierStrategy that interprets all subdomains of the given base URL as roots. + */ +export class SubdomainIdentifierStrategy extends BaseIdentifierStrategy { + private readonly baseUrl: string; + private readonly regex: RegExp; + protected readonly logger = getLoggerFor(this); + + public constructor(baseUrl: string) { + super(); + this.baseUrl = ensureTrailingSlash(baseUrl); + this.regex = createSubdomainRegexp(this.baseUrl); + } + + public supportsIdentifier(identifier: ResourceIdentifier): boolean { + const supported = this.regex.test(identifier.path); + this.logger.debug(supported ? + `Identifier ${identifier.path} is part of ${this.baseUrl}` : + `Identifier ${identifier.path} is not part of ${this.baseUrl}`); + return supported; + } + + public isRootContainer(identifier: ResourceIdentifier): boolean { + const match = this.regex.exec(identifier.path); + return Array.isArray(match) && match[0].length === identifier.path.length; + } +} diff --git a/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts b/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts new file mode 100644 index 000000000..5e03c8905 --- /dev/null +++ b/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts @@ -0,0 +1,31 @@ +import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; +import { BaseIdentifierStrategy } from '../../../../src/util/identifiers/BaseIdentifierStrategy'; + +class DummyStrategy extends BaseIdentifierStrategy { + public supportsIdentifier(identifier: ResourceIdentifier): boolean { + return !identifier.path.endsWith('unsupported'); + } + + public isRootContainer(identifier: ResourceIdentifier): boolean { + return identifier.path.endsWith('root'); + } +} + +describe('A BaseIdentifierStrategy', (): void => { + const strategy = new DummyStrategy(); + + it('returns the parent identifier.', async(): Promise => { + expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' }); + expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' }); + }); + + it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise => { + expect((): any => strategy.getParentContainer({ path: '/unsupported' })) + .toThrow('/unsupported is not supported'); + }); + + it('errors when attempting to get the parent of a root container.', async(): Promise => { + expect((): any => strategy.getParentContainer({ path: 'http://test.com/root' })) + .toThrow('http://test.com/root is a root container and has no parent'); + }); +}); diff --git a/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts b/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts index 9c0df063f..f86ef1608 100644 --- a/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts +++ b/test/unit/util/identifiers/SingleRootIdentifierStrategy.test.ts @@ -2,32 +2,17 @@ import { SingleRootIdentifierStrategy } from '../../../../src/util/identifiers/S describe('A SingleRootIdentifierStrategy', (): void => { const baseUrl = 'http://test.com/'; - const manager = new SingleRootIdentifierStrategy(baseUrl); + const strategy = new SingleRootIdentifierStrategy(baseUrl); it('verifies if identifiers are in its domain.', async(): Promise => { - expect(manager.supportsIdentifier({ path: 'http://notest.com/' })).toBe(false); - expect(manager.supportsIdentifier({ path: baseUrl })).toBe(true); - expect(manager.supportsIdentifier({ path: `${baseUrl}foo/bar` })).toBe(true); - }); - - it('returns the parent identifier.', async(): Promise => { - expect(manager.getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' }); - expect(manager.getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' }); - }); - - it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise => { - expect((): any => manager.getParentContainer({ path: 'http://nottest.com/' })) - .toThrow('http://nottest.com/ is not supported'); - }); - - it('errors when attempting to get the parent of a root container.', async(): Promise => { - expect((): any => manager.getParentContainer({ path: 'http://test.com/' })) - .toThrow('http://test.com/ is a root container and has no parent'); + expect(strategy.supportsIdentifier({ path: 'http://notest.com/' })).toBe(false); + expect(strategy.supportsIdentifier({ path: baseUrl })).toBe(true); + expect(strategy.supportsIdentifier({ path: `${baseUrl}foo/bar` })).toBe(true); }); it('checks for the root container by comparing with the base URL.', async(): Promise => { - expect(manager.isRootContainer({ path: 'http://notest.com/' })).toBe(false); - expect(manager.isRootContainer({ path: baseUrl })).toBe(true); - expect(manager.isRootContainer({ path: `${baseUrl}foo/bar` })).toBe(false); + expect(strategy.isRootContainer({ path: 'http://notest.com/' })).toBe(false); + expect(strategy.isRootContainer({ path: baseUrl })).toBe(true); + expect(strategy.isRootContainer({ path: `${baseUrl}foo/bar` })).toBe(false); }); }); diff --git a/test/unit/util/identifiers/SubdomainIdentifierStrategy.test.ts b/test/unit/util/identifiers/SubdomainIdentifierStrategy.test.ts new file mode 100644 index 000000000..bf5c86910 --- /dev/null +++ b/test/unit/util/identifiers/SubdomainIdentifierStrategy.test.ts @@ -0,0 +1,32 @@ +import { SubdomainIdentifierStrategy } from '../../../../src/util/identifiers/SubdomainIdentifierStrategy'; + +describe('A SubdomainIdentifierStrategy', (): void => { + const baseUrl = 'http://test.com/foo/'; + const strategy = new SubdomainIdentifierStrategy(baseUrl); + + it('supports URLs in its domain.', async(): Promise => { + expect(strategy.supportsIdentifier({ path: 'http://test.com/foo/' })).toBe(true); + expect(strategy.supportsIdentifier({ path: 'http://alice.test.com/foo/' })).toBe(true); + expect(strategy.supportsIdentifier({ path: 'http://a.b.c.test.com/foo/' })).toBe(true); + + expect(strategy.supportsIdentifier({ path: 'http://test.com/foo/bar' })).toBe(true); + expect(strategy.supportsIdentifier({ path: 'http://alice.test.com/foo/bar' })).toBe(true); + expect(strategy.supportsIdentifier({ path: 'http://a.b.c.test.com/foo/bar' })).toBe(true); + }); + + it('does not support URLs outside of its domain.', async(): Promise => { + expect(strategy.supportsIdentifier({ path: 'http://fake.com/http://test.com/foo/' })).toBe(false); + expect(strategy.supportsIdentifier({ path: 'http://fake.com/test.com/foo/' })).toBe(false); + expect(strategy.supportsIdentifier({ path: 'http://faketest.com/foo/' })).toBe(false); + expect(strategy.supportsIdentifier({ path: 'http://test.com/foo' })).toBe(false); + expect(strategy.supportsIdentifier({ path: 'ftp://test.com/foo/' })).toBe(false); + }); + + it('identifiers the base and all subdomains as root containers.', async(): Promise => { + expect(strategy.isRootContainer({ path: 'http://test.com/foo/' })).toBe(true); + expect(strategy.isRootContainer({ path: 'http://alice.test.com/foo/' })).toBe(true); + + expect(strategy.isRootContainer({ path: 'http://test.com/foo/bar' })).toBe(false); + expect(strategy.isRootContainer({ path: 'http://alice.test.com/foo/bar' })).toBe(false); + }); +});