Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ <h3>{{ 'collection.edit.tabs.source.form.head' | translate }}</h3>
(cancel)="onCancel()"></ds-form>
}
</div>
@if (contentSource && (contentSource?.harvestType === harvestTypeMetadataAndBitstreams)) {
<div class="container mt-2">
<div class="row">
<div class="col-12">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="allowExternalUrlsCheck"
[checked]="contentSource.allowExternalUrls" (change)="changeAllowExternalUrls()"
[attr.aria-describedby]="'allowExternalUrlsWarning'">
<label class="form-check-label"
for="allowExternalUrlsCheck">{{ 'collection.edit.tabs.source.allow-external-urls' | translate }}</label>
</div>
<!-- Warning styling only in the risky state; no role="alert", this is static help text -->
<div id="allowExternalUrlsWarning" class="mt-2"
[ngClass]="contentSource.allowExternalUrls ? 'alert alert-warning' : 'form-text'">
{{ 'collection.edit.tabs.source.allow-external-urls.warning' | translate }}
</div>
</div>
</div>
</div>
}
@if ((contentSource?.harvestType !== harvestTypeNone)) {
<div class="container mt-2">
<div class="row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AsyncPipe,
Location,
NgClass,
} from '@angular/common';
import {
Component,
Expand Down Expand Up @@ -79,6 +80,7 @@ import { CollectionSourceControlsComponent } from './collection-source-controls/
BtnDisabledDirective,
CollectionSourceControlsComponent,
FormComponent,
NgClass,
ThemedLoadingComponent,
TranslateModule,
],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/app/core/shared/content-source.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
6 changes: 6 additions & 0 deletions src/assets/i18n/cs.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
4 changes: 4 additions & 0 deletions src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading