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 @@
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.",