diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html index 7f392b2f027..55f8fd18ad9 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -52,6 +52,26 @@

{{ 'collection.edit.tabs.source.form.head' | translate }}

(cancel)="onCancel()"> } +@if (contentSource && (contentSource?.harvestType === harvestTypeMetadataAndBitstreams)) { +
+
+
+
+ + +
+ +
+ {{ 'collection.edit.tabs.source.allow-external-urls.warning' | translate }} +
+
+
+
+} @if ((contentSource?.harvestType !== harvestTypeNone)) {
diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index 3e61103ff60..82a43934f45 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -214,6 +214,72 @@ describe('CollectionSourceComponent', () => { }); }); + describe('when selecting the allow external URLs checkbox', () => { + let input; + + beforeEach(() => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndBitstreams; + fixture.detectChanges(); + input = fixture.debugElement.query(By.css('#allowExternalUrlsCheck')).nativeElement; + input.click(); + fixture.detectChanges(); + }); + + it('should enable allowExternalUrls', () => { + expect(comp.contentSource.allowExternalUrls).toBeTrue(); + }); + + it('should send a field update', () => { + expect(objectUpdatesService.saveAddFieldUpdate).toHaveBeenCalledWith(router.url, comp.contentSource); + }); + }); + + describe('the allow external URLs checkbox and warning', () => { + it('should be hidden when only metadata is harvested', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.Metadata; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsCheck'))).toBeNull(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsWarning'))).toBeNull(); + }); + + it('should be hidden when only references to bitstreams are harvested', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndRef; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsCheck'))).toBeNull(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsWarning'))).toBeNull(); + }); + + it('should be shown when bitstreams are harvested', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndBitstreams; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsCheck'))).not.toBeNull(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsWarning'))).not.toBeNull(); + }); + + it('should describe the checkbox with the warning, so screen readers announce the risk', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndBitstreams; + fixture.detectChanges(); + const checkbox = fixture.debugElement.query(By.css('#allowExternalUrlsCheck')).nativeElement; + expect(checkbox.getAttribute('aria-describedby')).toEqual('allowExternalUrlsWarning'); + }); + + it('should show the text unstyled before the checkbox is ticked', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndBitstreams; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('#allowExternalUrlsCheck')).nativeElement.checked).toBeFalse(); + const warning = fixture.debugElement.query(By.css('#allowExternalUrlsWarning')).nativeElement; + expect(warning.classList.contains('alert-warning')).toBeFalse(); + }); + + it('should style the text as a warning once the checkbox is ticked', () => { + comp.contentSource.harvestType = ContentSourceHarvestType.MetadataAndBitstreams; + comp.contentSource.allowExternalUrls = true; + fixture.detectChanges(); + const warning = fixture.debugElement.query(By.css('#allowExternalUrlsWarning')).nativeElement; + expect(warning.classList.contains('alert-warning')).toBeTrue(); + }); + }); + describe('isValid', () => { it('should return true when ContentSource is disabled but the form invalid', () => { spyOnProperty(comp.formGroup, 'valid').and.returnValue(false); diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts index fa20b0e5e12..25103fd8c47 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,6 +1,7 @@ import { AsyncPipe, Location, + NgClass, } from '@angular/common'; import { Component, @@ -79,6 +80,7 @@ import { CollectionSourceControlsComponent } from './collection-source-controls/ BtnDisabledDirective, CollectionSourceControlsComponent, FormComponent, + NgClass, ThemedLoadingComponent, TranslateModule, ], @@ -254,6 +256,11 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ harvestTypeNone = ContentSourceHarvestType.None; + /** + * The content harvesting type that downloads bitstreams, the only one the external URL flag applies to + */ + harvestTypeMetadataAndBitstreams = ContentSourceHarvestType.MetadataAndBitstreams; + /** * The previously selected harvesting type * Used for switching between ContentSourceHarvestType.None and the previously selected value when enabling / disabling harvesting @@ -465,6 +472,16 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.updateContentSource(false); } + /** + * Switch the allowExternalUrls flag on or off and fire a field update + * Deliberately not a dynamic form control: the patchValue() in initializeOriginalContentSource() only names the + * three form containers, so a control here would not revert on Discard/Reinstate + */ + changeAllowExternalUrls() { + this.contentSource.allowExternalUrls = !this.contentSource.allowExternalUrls; + this.saveFieldUpdate(); + } + /** * Loop over all inputs and update the Content Source with their value * @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index cfc78f223fc..3434858c53d 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -73,6 +73,13 @@ export class ContentSource extends CacheableObject { @autoserializeAs('harvest_type') harvestType = ContentSourceHarvestType.None; + /** + * Whether bitstreams may be fetched from hosts other than the OAI provider + * Initialised to false on purpose: cerialize omits undefined values, so without it the key would never reach the backend + */ + @autoserializeAs('allow_external_urls') + allowExternalUrls = false; + /** * The available metadata configurations */ diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 0e8b47fda3e..4d70fc4fc83 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1916,6 +1916,12 @@ // "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.roles.title": "Upravit kolekci - Role", + // "collection.edit.tabs.source.allow-external-urls": "Allow external URLs", + "collection.edit.tabs.source.allow-external-urls": "Povolit externí URL", + + // "collection.edit.tabs.source.allow-external-urls.warning": "When off, files are fetched only from the same scheme and host as the OAI provider above, and a record pointing elsewhere is skipped in full. When on, your server fetches from any address the provider names — that provider, not you, decides what is downloaded and republished here, so enable it only for providers you trust. Private and internal addresses stay blocked either way.", + "collection.edit.tabs.source.allow-external-urls.warning": "Když je vypnuto, stahují se soubory jen ze stejného schématu a hostitele, jaké má adresa poskytovatele OAI výše; záznam odkazující jinam se přeskočí celý. Když je zapnuto, stahuje váš server z libovolné adresy, kterou poskytovatel uvede — o tom, co se zde stáhne a znovu zveřejní, tak rozhoduje on, a ne vy. Zapínejte jen u poskytovatelů, kterým důvěřujete. Privátní a interní adresy zůstávají blokované tak či tak.", + // "collection.edit.tabs.source.external": "This collection harvests its content from an external source", "collection.edit.tabs.source.external": "Tato kolekce získává svůj obsah z externího zdroje", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2cb600f2731..b3c8c5c1b88 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1277,6 +1277,10 @@ "collection.edit.tabs.roles.title": "Collection Edit - Roles", + "collection.edit.tabs.source.allow-external-urls": "Allow external URLs", + + "collection.edit.tabs.source.allow-external-urls.warning": "When off, files are fetched only from the same scheme and host as the OAI provider above, and a record pointing elsewhere is skipped in full. When on, your server fetches from any address the provider names — that provider, not you, decides what is downloaded and republished here, so enable it only for providers you trust. Private and internal addresses stay blocked either way.", + "collection.edit.tabs.source.external": "This collection harvests its content from an external source", "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.",