From f4a3301fabc7ce7864fe98b4afb13a78e58a32d5 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Thu, 28 May 2026 14:53:28 +0200 Subject: [PATCH 1/3] Author redirect based on configuration - browse/orcid --- ...-text-metadata-list-element.component.html | 45 +++++--- ...-text-metadata-list-element.component.scss | 8 ++ ...in-text-metadata-list-element.component.ts | 92 +++++++++------- src/app/shared/utils/orcid-author.util.ts | 100 ++++++++++++++++++ src/assets/i18n/cs.json5 | 2 + src/assets/i18n/en.json5 | 1 + 6 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 src/app/shared/utils/orcid-author.util.ts diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index d8a44461529..01f51191978 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -8,20 +8,41 @@ {{mdRepresentation.getValue()}} - - - {{mdRepresentation.getValue()}} - - - - {{mdRepresentation.getValue()}} + + + + + {{mdRepresentation.getValue()}} + + + + + + + + {{mdRepresentation.getValue()}} + + + + {{mdRepresentation.getValue()}} + + - + {{mdRepresentation.getValue()}} diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.scss b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.scss index a32f07ac5a3..5643ec592c2 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.scss +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.scss @@ -6,6 +6,14 @@ } } +.orcid-icon-link { + text-decoration: none; + + &:hover { + opacity: 0.8; + } +} + .orcid-icon { color: #a6ce39; // Official ORCID green color font-size: 1em; diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts index cdc0fd7ee5c..189ecc553f3 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -4,9 +4,16 @@ import { BehaviorSubject } from 'rxjs'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; -import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type'; import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator'; +import { + AuthorOrcidLinkTarget, + buildOrcidProfileUrl, + DEFAULT_AUTHOR_ORCID_LINK_TARGET, + isOrcidAuthorityValue, + loadAuthorOrcidLinkTarget, + loadOrcidDomainUrl, +} from '../../../utils/orcid-author.util'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; @metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText) @@ -24,30 +31,23 @@ import { MetadataRepresentationListElementComponent } from '../metadata-represen export class PlainTextMetadataListElementComponent extends MetadataRepresentationListElementComponent implements OnInit { /** - * Regex pattern for ORCID identifiers: four groups of four digits separated by hyphens. - * The last group may end with an X (checksum digit). + * ORCID domain URL loaded from the backend. */ - private static readonly ORCID_PATTERN = /^\d{4}-\d{4}-\d{4}-(\d{3}X|\d{4})$/; - orcidDomainUrl$ = new BehaviorSubject(null); + /** + * Target of the link rendered on the author name for an ORCID author. Loaded from the + * backend property `item.author.orcid.link-target`. + */ + authorOrcidLinkTarget$ = new BehaviorSubject(DEFAULT_AUTHOR_ORCID_LINK_TARGET); + constructor(private configurationService: ConfigurationDataService) { super(); } ngOnInit(): void { - this.configurationService.findByPropertyName('orcid.domain-url').pipe( - getFirstCompletedRemoteData(), - ).subscribe((rd) => { - if (rd.hasFailed || !rd.hasSucceeded || !rd.payload?.values?.length) { - return; - } - - const url = rd.payload.values[0]?.trim(); - if (url && /^https?:\/\//i.test(url)) { - this.orcidDomainUrl$.next(url); - } - }); + loadOrcidDomainUrl(this.configurationService).then((url) => this.orcidDomainUrl$.next(url)); + loadAuthorOrcidLinkTarget(this.configurationService).then((t) => this.authorOrcidLinkTarget$.next(t)); } /** @@ -62,30 +62,46 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio return queryParams; } - isOrcidAuthority(orcidDomainUrl: string | null): boolean { - if (orcidDomainUrl === null) { - return false; - } - if (this.mdRepresentation instanceof MetadatumRepresentation) { - const authority = this.mdRepresentation.authority?.trim(); - return !!authority && PlainTextMetadataListElementComponent.ORCID_PATTERN.test(authority); - } - return false; + /** + * Query parameters for the browse link of an authority-controlled value (e.g. ORCID author). + * Passes both `value` and `authority` so the browse page can call the REST endpoint with + * `filterValue` + `filterAuthority` and resolve items whose metadata is indexed by authority key. + */ + getAuthorityBrowseQueryParams() { + return { + value: this.mdRepresentation.getValue(), + authority: this.getAuthority(), + }; } - getOrcidUrl(orcidDomainUrl: string | null): string { - if (orcidDomainUrl === null) { - return ''; - } - const authority = this.mdRepresentation instanceof MetadatumRepresentation - ? this.mdRepresentation.authority?.trim() - : undefined; + /** + * True when the current metadatum carries a browse definition (e.g. `dc.contributor.author`), + * which means the value can be rendered as a clickable browse link. + */ + hasBrowseDefinition(): boolean { + return !!this.mdRepresentation?.browseDefinition; + } - if (!authority || !PlainTextMetadataListElementComponent.ORCID_PATTERN.test(authority)) { - return ''; - } + /** + * Check whether the authority value of this metadata is an ORCID identifier. + * Accepts either a bare ORCID iD or a full ORCID URL. + */ + isOrcidAuthority(): boolean { + return isOrcidAuthorityValue(this.getAuthority(), this.orcidDomainUrl$.value); + } - const base = orcidDomainUrl.endsWith('/') ? orcidDomainUrl : orcidDomainUrl + '/'; - return `${base}${authority}`; + /** + * Build the full ORCID profile URL for the current author. Returns an empty string when + * the authority is not an ORCID value or when the ORCID domain URL is required but missing. + */ + getOrcidUrl(): string { + return buildOrcidProfileUrl(this.getAuthority(), this.orcidDomainUrl$.value); + } + + private getAuthority(): string | undefined { + if (this.mdRepresentation instanceof MetadatumRepresentation) { + return this.mdRepresentation.authority?.trim(); + } + return undefined; } } diff --git a/src/app/shared/utils/orcid-author.util.ts b/src/app/shared/utils/orcid-author.util.ts new file mode 100644 index 00000000000..ea113195a33 --- /dev/null +++ b/src/app/shared/utils/orcid-author.util.ts @@ -0,0 +1,100 @@ +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; + +/** + * Pattern that matches a bare ORCID iD (16 digits in groups of 4, last char may be X). + * Example: `0000-0001-2345-6789` or `0000-0001-2345-678X`. + */ +export const ORCID_ID_PATTERN = /^(\d{4}-){3}\d{3}[\dX]$/i; + +/** + * Pattern that matches a full ORCID URL authority value. + * Example: `https://orcid.org/0000-0001-2345-6789`. + */ +export const ORCID_URL_PATTERN = /^https?:\/\/[^/]+\/((\d{4}-){3}\d{3}[\dX])$/i; + +/** + * Type of the backend property `item.author.orcid.link-target`. + * Controls the target of the link rendered on the author name for an ORCID-authority author. + * Any unknown / unset value falls back to the default (`browse`). + */ +export type AuthorOrcidLinkTarget = string; + +/** + * Name of the backend configuration property that controls the author-name link target + * for ORCID-authority authors. + */ +export const AUTHOR_ORCID_LINK_TARGET_PROPERTY = 'item.author.orcid.link-target'; + +/** + * Default value used when `item.author.orcid.link-target` is unset, invalid, or unreachable. + */ +export const DEFAULT_AUTHOR_ORCID_LINK_TARGET: AuthorOrcidLinkTarget = 'browse'; + +/** + * Load `orcid.domain-url` from the backend configuration. Returns null when unset + * or when the value does not look like an absolute http(s) URL. + */ +export function loadOrcidDomainUrl( + configurationService: ConfigurationDataService, +): Promise { + return configurationService.findByPropertyName('orcid.domain-url') + .pipe(getFirstSucceededRemoteDataPayload()) + .toPromise() + .then((rd: any) => { + const value = rd?.values?.[0]?.trim(); + return value && /^https?:\/\//i.test(value) ? value : null; + }) + .catch(() => null); +} + +/** + * Load `item.author.orcid.link-target` from the backend configuration. + * Returns the default (`browse`) when the property is unset or invalid. + */ +export function loadAuthorOrcidLinkTarget( + configurationService: ConfigurationDataService, +): Promise { + return configurationService.findByPropertyName(AUTHOR_ORCID_LINK_TARGET_PROPERTY) + .pipe(getFirstSucceededRemoteDataPayload()) + .toPromise() + .then((rd: any) => { + const value = rd?.values?.[0]?.trim()?.toLowerCase(); + return value ? value : DEFAULT_AUTHOR_ORCID_LINK_TARGET; + }) + .catch(() => DEFAULT_AUTHOR_ORCID_LINK_TARGET); +} + +/** + * True when the authority string is a recognised ORCID value (bare iD or full URL). + * Bare iDs require `orcidDomainUrl` so the link can be built without guessing the domain. + */ +export function isOrcidAuthorityValue(authority: string | undefined | null, orcidDomainUrl: string | null): boolean { + if (!authority) { + return false; + } + const trimmed = authority.trim(); + if (ORCID_URL_PATTERN.test(trimmed)) { + return true; + } + return !!orcidDomainUrl && ORCID_ID_PATTERN.test(trimmed); +} + +/** + * Build the full ORCID profile URL for the given authority. Returns an empty string when + * the authority is not an ORCID value or when the ORCID domain URL is required but missing. + */ +export function buildOrcidProfileUrl(authority: string | undefined | null, orcidDomainUrl: string | null): string { + if (!authority) { + return ''; + } + const trimmed = authority.trim(); + if (ORCID_URL_PATTERN.test(trimmed)) { + return trimmed; + } + if (orcidDomainUrl && ORCID_ID_PATTERN.test(trimmed)) { + const domain = orcidDomainUrl.endsWith('/') ? orcidDomainUrl.slice(0, -1) : orcidDomainUrl; + return `${domain}/${trimmed}`; + } + return ''; +} diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 7a10a5ce310..52780715b6e 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -3423,6 +3423,8 @@ "item.view.box.author.preview.and": "a", // "item.view.box.author.preview.show-everyone": "show everyone:", "item.view.box.author.preview.show-everyone": "zobraz všechny autory", + // "item.view.box.author.preview.orcid-link.title": "View ORCID profile", + "item.view.box.author.preview.orcid-link.title": "Zobrazit ORCID profil", // "item.file.description.not.supported.video": "Your browser does not support the video tag.", "item.file.description.not.supported.video": "Váš prohlížeč nepodporuje videa.", // "item.file.description.name": "Name", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 37e9d8f94b1..1283f0656c9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2868,6 +2868,7 @@ "item.view.box.author.preview.show-everyone": "show everyone", + "item.view.box.author.preview.orcid-link.title": "View ORCID profile", "item.file.description.not.supported.video": "Your browser does not support the video tag.", From 6dff93df3a67c4da82e6e0010f61c9ac229fdd9c Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Thu, 28 May 2026 15:05:47 +0200 Subject: [PATCH 2/3] Changed name of property --- .../plain-text-metadata-list-element.component.ts | 2 +- src/app/shared/utils/orcid-author.util.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts index 189ecc553f3..e1637cd18d2 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -37,7 +37,7 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio /** * Target of the link rendered on the author name for an ORCID author. Loaded from the - * backend property `item.author.orcid.link-target`. + * backend property `orcid.author.link-target`. */ authorOrcidLinkTarget$ = new BehaviorSubject(DEFAULT_AUTHOR_ORCID_LINK_TARGET); diff --git a/src/app/shared/utils/orcid-author.util.ts b/src/app/shared/utils/orcid-author.util.ts index ea113195a33..9e7c9e2724f 100644 --- a/src/app/shared/utils/orcid-author.util.ts +++ b/src/app/shared/utils/orcid-author.util.ts @@ -14,7 +14,7 @@ export const ORCID_ID_PATTERN = /^(\d{4}-){3}\d{3}[\dX]$/i; export const ORCID_URL_PATTERN = /^https?:\/\/[^/]+\/((\d{4}-){3}\d{3}[\dX])$/i; /** - * Type of the backend property `item.author.orcid.link-target`. + * Type of the backend property `orcid.author.link-target`. * Controls the target of the link rendered on the author name for an ORCID-authority author. * Any unknown / unset value falls back to the default (`browse`). */ @@ -24,10 +24,10 @@ export type AuthorOrcidLinkTarget = string; * Name of the backend configuration property that controls the author-name link target * for ORCID-authority authors. */ -export const AUTHOR_ORCID_LINK_TARGET_PROPERTY = 'item.author.orcid.link-target'; +export const AUTHOR_ORCID_LINK_TARGET_PROPERTY = 'orcid.author.link-target'; /** - * Default value used when `item.author.orcid.link-target` is unset, invalid, or unreachable. + * Default value used when `orcid.author.link-target` is unset, invalid, or unreachable. */ export const DEFAULT_AUTHOR_ORCID_LINK_TARGET: AuthorOrcidLinkTarget = 'browse'; @@ -49,7 +49,7 @@ export function loadOrcidDomainUrl( } /** - * Load `item.author.orcid.link-target` from the backend configuration. + * Load `orcid.author.link-target` from the backend configuration. * Returns the default (`browse`) when the property is unset or invalid. */ export function loadAuthorOrcidLinkTarget( From 575f73de7facc56ffb8da2d6795fd666ff3e1acf Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Thu, 28 May 2026 16:29:09 +0200 Subject: [PATCH 3/3] Update spec --- ...xt-metadata-list-element.component.spec.ts | 35 +++++++++++-------- src/app/shared/utils/orcid-author.util.ts | 25 +++++++++++-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts index 1a3d04b7865..ca24e8a20e6 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; @@ -34,9 +35,12 @@ const mockOrcidWithWhitespaceRepresentation = Object.assign(new MetadatumReprese }); const mockConfigurationDataService = { - findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue( - createSuccessfulRemoteDataObject$({ values: ['https://orcid.org'] }), - ), + findByPropertyName: jasmine.createSpy('findByPropertyName').and.callFake((property: string) => { + if (property === 'orcid.author.link-target') { + return createSuccessfulRemoteDataObject$({ values: ['browse'] }); + } + return createSuccessfulRemoteDataObject$({ values: ['https://orcid.org'] }); + }), }; describe('PlainTextMetadataListElementComponent', () => { @@ -45,7 +49,7 @@ describe('PlainTextMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [], + imports: [TranslateModule.forRoot()], declarations: [PlainTextMetadataListElementComponent], providers: [ { provide: ConfigurationDataService, useValue: mockConfigurationDataService }, @@ -74,6 +78,7 @@ describe('PlainTextMetadataListElementComponent', () => { describe('when metadata has ORCID authority', () => { beforeEach(() => { comp.mdRepresentation = mockOrcidRepresentation; + comp.authorOrcidLinkTarget$.next('orcid'); fixture.detectChanges(); }); @@ -90,11 +95,11 @@ describe('PlainTextMetadataListElementComponent', () => { }); it('isOrcidAuthority should return true', () => { - expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeTrue(); + expect(comp.isOrcidAuthority()).toBeTrue(); }); it('getOrcidUrl should return full ORCID URL', () => { - expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe('https://orcid.org/1234-5678-9012-3456'); + expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456'); }); }); @@ -116,7 +121,7 @@ describe('PlainTextMetadataListElementComponent', () => { }); it('isOrcidAuthority should return false', () => { - expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeFalse(); + expect(comp.isOrcidAuthority()).toBeFalse(); }); }); @@ -124,13 +129,13 @@ describe('PlainTextMetadataListElementComponent', () => { it('should not double-slash when domain URL ends with /', () => { comp.orcidDomainUrl$.next('https://orcid.org/'); comp.mdRepresentation = mockOrcidRepresentation; - expect(comp.getOrcidUrl('https://orcid.org/')).toBe('https://orcid.org/1234-5678-9012-3456'); + expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456'); }); it('should add slash when domain URL does not end with /', () => { comp.orcidDomainUrl$.next('https://sandbox.orcid.org'); comp.mdRepresentation = mockOrcidRepresentation; - expect(comp.getOrcidUrl('https://sandbox.orcid.org')).toBe('https://sandbox.orcid.org/1234-5678-9012-3456'); + expect(comp.getOrcidUrl()).toBe('https://sandbox.orcid.org/1234-5678-9012-3456'); }); }); @@ -147,11 +152,11 @@ describe('PlainTextMetadataListElementComponent', () => { }); it('isOrcidAuthority should return false', () => { - expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeFalse(); + expect(comp.isOrcidAuthority()).toBeFalse(); }); it('getOrcidUrl should return empty string', () => { - expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe(''); + expect(comp.getOrcidUrl()).toBe(''); }); }); @@ -159,13 +164,13 @@ describe('PlainTextMetadataListElementComponent', () => { it('should return empty string when orcidDomainUrl is null', () => { comp.orcidDomainUrl$.next(null); comp.mdRepresentation = mockOrcidRepresentation; - expect(comp.getOrcidUrl(null)).toBe(''); + expect(comp.getOrcidUrl()).toBe(''); }); it('should return empty string when mdRepresentation has no authority', () => { comp.orcidDomainUrl$.next('https://orcid.org'); comp.mdRepresentation = mockMetadataRepresentation; - expect(comp.getOrcidUrl('https://orcid.org')).toBe(''); + expect(comp.getOrcidUrl()).toBe(''); }); }); @@ -177,11 +182,11 @@ describe('PlainTextMetadataListElementComponent', () => { }); it('isOrcidAuthority should return true after trimming', () => { - expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeTrue(); + expect(comp.isOrcidAuthority()).toBeTrue(); }); it('getOrcidUrl should return trimmed ORCID URL', () => { - expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe('https://orcid.org/1234-5678-9012-3456'); + expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456'); }); }); }); diff --git a/src/app/shared/utils/orcid-author.util.ts b/src/app/shared/utils/orcid-author.util.ts index 9e7c9e2724f..224d43fcb65 100644 --- a/src/app/shared/utils/orcid-author.util.ts +++ b/src/app/shared/utils/orcid-author.util.ts @@ -65,9 +65,24 @@ export function loadAuthorOrcidLinkTarget( .catch(() => DEFAULT_AUTHOR_ORCID_LINK_TARGET); } +/** + * Extract the lowercase host of an absolute http(s) URL. Returns null on invalid input. + */ +function getHost(url: string | null | undefined): string | null { + if (!url) { + return null; + } + try { + return new URL(url).host.toLowerCase(); + } catch { + return null; + } +} + /** * True when the authority string is a recognised ORCID value (bare iD or full URL). * Bare iDs require `orcidDomainUrl` so the link can be built without guessing the domain. + * URL authorities are only accepted when their host matches the configured ORCID domain. */ export function isOrcidAuthorityValue(authority: string | undefined | null, orcidDomainUrl: string | null): boolean { if (!authority) { @@ -75,7 +90,8 @@ export function isOrcidAuthorityValue(authority: string | undefined | null, orci } const trimmed = authority.trim(); if (ORCID_URL_PATTERN.test(trimmed)) { - return true; + const expectedHost = getHost(orcidDomainUrl); + return !!expectedHost && getHost(trimmed) === expectedHost; } return !!orcidDomainUrl && ORCID_ID_PATTERN.test(trimmed); } @@ -83,6 +99,7 @@ export function isOrcidAuthorityValue(authority: string | undefined | null, orci /** * Build the full ORCID profile URL for the given authority. Returns an empty string when * the authority is not an ORCID value or when the ORCID domain URL is required but missing. + * URL authorities are only accepted when their host matches the configured ORCID domain. */ export function buildOrcidProfileUrl(authority: string | undefined | null, orcidDomainUrl: string | null): string { if (!authority) { @@ -90,7 +107,11 @@ export function buildOrcidProfileUrl(authority: string | undefined | null, orcid } const trimmed = authority.trim(); if (ORCID_URL_PATTERN.test(trimmed)) { - return trimmed; + const expectedHost = getHost(orcidDomainUrl); + if (expectedHost && getHost(trimmed) === expectedHost) { + return trimmed; + } + return ''; } if (orcidDomainUrl && ORCID_ID_PATTERN.test(trimmed)) { const domain = orcidDomainUrl.endsWith('/') ? orcidDomainUrl.slice(0, -1) : orcidDomainUrl;