From b16256bb95cfa306447eeb46854cf78345834db2 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 12:48:44 +0200 Subject: [PATCH 1/8] Fetch th refbox content from the BE --- .../clarin-ref-citation.component.html | 6 +- .../clarin-ref-citation.component.ts | 187 ++++-------------- 2 files changed, 34 insertions(+), 159 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html index 407f1963e93..ac481810c59 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html @@ -11,11 +11,7 @@
-
- {{citationText + ', '}} - {{itemNameText + ', '}} - {{repositoryNameText + ', '}} - {{prettifiedIdentifier | async}} +
diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index a9cdbb1f0e5..e63b212f511 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -1,7 +1,6 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Item } from '../../core/shared/item.model'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { isEmpty, isNotEmpty, isNull, isUndefined } from '../../shared/empty.util'; import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { Clipboard } from '@angular/cdk/clipboard'; import { NgbModal, NgbTooltip, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; @@ -10,17 +9,9 @@ import { GetRequest } from '../../core/data/request.models'; import { RequestService } from '../../core/data/request.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { BehaviorSubject } from 'rxjs'; -import { - DOI_METADATA_FIELD, HANDLE_METADATA_FIELD, -} from '../simple/field-components/clarin-generic-item-field/clarin-generic-item-field.component'; +import { BehaviorSubject, of } from 'rxjs'; import { ItemIdentifierService } from '../../shared/item-identifier.service'; -import { AUTHOR_METADATA_FIELDS } from '../../core/shared/clarin/constants'; -/** - * If the item has more authors do not add all authors to the citation but add there a shortcut. - */ -export const ET_AL_TEXT = 'et al.'; /** * The citation part in the ref-box component. @@ -44,39 +35,22 @@ export class ClarinRefCitationComponent implements OnInit { */ @ViewChild('tooltip', {static: false}) tooltipRef: NgbTooltip; - /** - * The parameters retrieved from the Item metadata for creating the citation in the proper way. - */ - /** - * Author and issued year - */ - citationText: string; - /** - * Whole Handle URI - */ - identifierURI: string; /** * Name of the Item */ itemNameText: string; - /** - * The nam of the organization which provides the repository - */ - repositoryNameText: string; - /** - * BehaviorSubject to store the prettified identifier. - */ - prettifiedIdentifier: BehaviorSubject = new BehaviorSubject(null); - /** - * The item has DOI or not. - */ - hasDoi = false; /** * The authors of the item. Fetched from the metadata. */ authors: string[] = []; + /** + * The content of the reference box, which will be displayed in the tooltip. + * This content is fetched from the RefBox Controller. + */ + refboxContent: BehaviorSubject = new BehaviorSubject(null); + constructor(private configurationService: ConfigurationDataService, private clipboard: Clipboard, public config: NgbTooltipConfig, @@ -90,140 +64,45 @@ export class ClarinRefCitationComponent implements OnInit { } ngOnInit(): void { - this.authors = this.item.allMetadataValues(AUTHOR_METADATA_FIELDS); - // First Part could be authors or publisher - let firstPart = this.getAuthors(); - const year = this.getYear(); - - // Show publisher instead of author if author is none - if (isEmpty(firstPart)) { - firstPart = this.item.firstMetadataValue('dc.publisher'); - } - - let citationArray = [firstPart, year]; - // Filter null values - citationArray = citationArray.filter(textValue => { - return isNotEmpty(textValue); - }); - - this.hasDoi = this.hasItemDoi(); - this.citationText = citationArray.join(', '); - this.itemNameText = this.getTitle(); - this.identifierURI = this.getIdentifierUri(this.whichIdentifierMetadataField()); - void this.itemIdentifierService.prettifyIdentifier(this.identifierURI, [this.whichIdentifierMetadataField()]) - .then((value: string) => { - this.prettifiedIdentifier.next(value); + void this.fetchRefBoxContent() + .then((res) => { + this.refboxContent.next(res); }); - void this.getRepositoryName().then(res => { - this.repositoryNameText = res?.payload?.values?.[0]; - }); } /** - * After click on the `Copy` icon the text will be formatted and copied for the user. + * Copy the text from the reference box to the clipboard. + * Remove the html tags from the text and copy only the plain text. */ - copyText() { - const tabChar = ' '; - let authorWithItemName = this.citationText + ',\n' + tabChar + this.itemNameText; - this.clipboard.copy(authorWithItemName + ', ' + - this.repositoryNameText + ', \n' + tabChar + this.identifierURI); + async copyText() { + const displayText = this.refboxContent.value; + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = displayText; + const plainText = tempDiv.textContent || ''; + this.clipboard.copy(plainText); setTimeout(() => { this.tooltipRef.close(); }, 700); } - getRepositoryName(): Promise { - return this.configurationService.findByPropertyName('dspace.name') - .pipe(getFirstSucceededRemoteData()).toPromise(); - } - - /** - * Get the identifier URI from the item metadata. If the item has DOI, return the DOI, otherwise return the handle. - */ - getIdentifierUri(identifierMetadataField) { - return this.item.firstMetadataValue(identifierMetadataField); - } - - /** - * Check if the item has DOI. - */ - hasItemDoi() { - return this.item?.allMetadata(DOI_METADATA_FIELD)?.length > 0; - } - /** - * If the item has DOI, return the DOI metadata field, otherwise return the handle metadata field. + * Fetch the content of the reference box from the RefBox Controller. */ - whichIdentifierMetadataField() { - return this.hasDoi ? DOI_METADATA_FIELD : HANDLE_METADATA_FIELD; - } - - getHandle() { - // Separate the handle from the full URI - const fullUri = this.getIdentifierUri(this.whichIdentifierMetadataField()); - const handleWord = 'handle/'; - const startHandleIndex = fullUri.indexOf('handle/') + handleWord.length; - return fullUri.substr(startHandleIndex); - } - - /** - * Check if the Item has any author metadata. - * @param authorMetadata - */ - hasNoAuthor(authorMetadata: string[] = []) { - return isEmpty(authorMetadata); - } - - getAuthors() { - let authorText = ''; - const authorMetadata = this.authors; - if (isUndefined(authorMetadata) || isNull(authorMetadata)) { - return null; - } - - // If metadata value is `(:unav) Unknown author` return null - if (this.hasNoAuthor(authorMetadata)) { - return null; - } - - // If there is only one author - if (authorMetadata.length === 1) { - return authorMetadata[0]; - } - - // If there are less than 5 authors - if (authorMetadata.length <= 5) { - let authors_list = authorMetadata.join('; '); - // Replace last `;` with `and` - authors_list = authors_list.replace(/;([^;]*)$/, ' and$1'); - return authors_list; - } - - // If there are more than 5 authors - // Get only first author and add `et al.` at the end - authorText = authorMetadata[0] + '; ' + ET_AL_TEXT; - return authorText; - } - - getYear() { - const yearMetadata = this.item.metadata['dc.date.issued']; - if (isUndefined(yearMetadata) || isNull(yearMetadata)) { - return null; - } - - // The issued date is in the format '2000-01-01' - const issuedDateValues = yearMetadata[0]?.value?.split('-'); - // Extract the year and return - return issuedDateValues[0]; - } + async fetchRefBoxContent() { + const requestId = this.requestService.generateRequestId(); + const getRequest = new GetRequest( + requestId, + this.halService.getRootHref() + '/core/refbox?handle=' + this.item?.handle + ); + this.requestService.send(getRequest); - getTitle() { - const titleMetadata = this.item.metadata['dc.title']; - if (isUndefined(titleMetadata) || isNull(titleMetadata)) { - return null; + try { + const res: any = await this.rdbService.buildFromRequestUUID(requestId) + .pipe(getFirstSucceededRemoteData()).toPromise(); + return res?.payload?.displayText || ''; + } catch (error) { + return of('Cannot fetch the ref box content'); } - - return titleMetadata[0]?.value; } /** @@ -260,7 +139,7 @@ export class ClarinRefCitationComponent implements OnInit { // Create the request const getRequest = new GetRequest(requestId, this.halService.getRootHref() + '/core/refbox/citations?type=' + // citationType + '&handle=' + this.getHandle(), requestOptions); - citationType + '&handle=' + this.getHandle()); + citationType + '&handle=' + this.item?.handle); // Call get request this.requestService.send(getRequest); From 3ef2ad0a15405cf1814cbe8a9f5730879216f1b3 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 12:51:57 +0200 Subject: [PATCH 2/8] Remove commented out code --- .../clarin-ref-citation/clarin-ref-citation.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index e63b212f511..e727b4f2ac7 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -138,7 +138,6 @@ export class ClarinRefCitationComponent implements OnInit { const requestId = this.requestService.generateRequestId(); // Create the request const getRequest = new GetRequest(requestId, this.halService.getRootHref() + '/core/refbox/citations?type=' + - // citationType + '&handle=' + this.getHandle(), requestOptions); citationType + '&handle=' + this.item?.handle); // Call get request From 7b4e61b140c61e03c1b2d1cc3bb5e41698434deb Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 12:55:02 +0200 Subject: [PATCH 3/8] Show an error that the refbox content wasn't properly fetched when some error --- .../clarin-ref-citation/clarin-ref-citation.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index e727b4f2ac7..8a85fb6b555 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -96,12 +96,13 @@ export class ClarinRefCitationComponent implements OnInit { ); this.requestService.send(getRequest); + const EMPTY_CONTENT = 'Cannot fetch the ref box content'; try { const res: any = await this.rdbService.buildFromRequestUUID(requestId) .pipe(getFirstSucceededRemoteData()).toPromise(); - return res?.payload?.displayText || ''; + return res?.payload?.displayText || EMPTY_CONTENT; } catch (error) { - return of('Cannot fetch the ref box content'); + return of(EMPTY_CONTENT); } } From 7a1ac9583b73e80aedd5c50116edadd76999138f Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 13:07:29 +0200 Subject: [PATCH 4/8] Sanitize the code from the BE. Show errors and proper messages when something has failed --- .../clarin-ref-citation.component.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index 8a85fb6b555..916dee89b15 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -9,8 +9,8 @@ import { GetRequest } from '../../core/data/request.models'; import { RequestService } from '../../core/data/request.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { BehaviorSubject, of } from 'rxjs'; -import { ItemIdentifierService } from '../../shared/item-identifier.service'; +import { BehaviorSubject } from 'rxjs'; +import { DomSanitizer } from '@angular/platform-browser'; /** @@ -40,10 +40,6 @@ export class ClarinRefCitationComponent implements OnInit { */ itemNameText: string; - /** - * The authors of the item. Fetched from the metadata. - */ - authors: string[] = []; /** * The content of the reference box, which will be displayed in the tooltip. @@ -51,6 +47,11 @@ export class ClarinRefCitationComponent implements OnInit { */ refboxContent: BehaviorSubject = new BehaviorSubject(null); + /** + * The text to be displayed when the ref box content is empty or cannot be fetched. + */ + EMPTY_CONTENT = 'Cannot fetch the ref box content'; + constructor(private configurationService: ConfigurationDataService, private clipboard: Clipboard, public config: NgbTooltipConfig, @@ -58,15 +59,19 @@ export class ClarinRefCitationComponent implements OnInit { private requestService: RequestService, protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, - private itemIdentifierService: ItemIdentifierService) { + private sanitizer: DomSanitizer) { // Configure the tooltip to show on click - `Copied` message config.triggers = 'click'; } ngOnInit(): void { void this.fetchRefBoxContent() - .then((res) => { - this.refboxContent.next(res); + .then((content) => { + // Sanitize the content to prevent XSS attacks + this.refboxContent.next(this.sanitizer.bypassSecurityTrustHtml(content)); + }).catch((error) => { + console.error('Failed to fetch refbox content:', error); + this.refboxContent.next(this.EMPTY_CONTENT); }); } @@ -74,11 +79,14 @@ export class ClarinRefCitationComponent implements OnInit { * Copy the text from the reference box to the clipboard. * Remove the html tags from the text and copy only the plain text. */ - async copyText() { + copyText() { const displayText = this.refboxContent.value; - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = displayText; - const plainText = tempDiv.textContent || ''; + let plainText = this.EMPTY_CONTENT; + if (displayText) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = displayText; + plainText = tempDiv.textContent || ''; + } this.clipboard.copy(plainText); setTimeout(() => { this.tooltipRef.close(); @@ -88,7 +96,7 @@ export class ClarinRefCitationComponent implements OnInit { /** * Fetch the content of the reference box from the RefBox Controller. */ - async fetchRefBoxContent() { + async fetchRefBoxContent(): Promise { const requestId = this.requestService.generateRequestId(); const getRequest = new GetRequest( requestId, @@ -96,13 +104,12 @@ export class ClarinRefCitationComponent implements OnInit { ); this.requestService.send(getRequest); - const EMPTY_CONTENT = 'Cannot fetch the ref box content'; try { const res: any = await this.rdbService.buildFromRequestUUID(requestId) .pipe(getFirstSucceededRemoteData()).toPromise(); - return res?.payload?.displayText || EMPTY_CONTENT; + return res?.payload?.displayText || this.EMPTY_CONTENT; } catch (error) { - return of(EMPTY_CONTENT); + return this.EMPTY_CONTENT; } } From 8449e969b73447305037b545d4012b3e826ee05d Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 13:09:28 +0200 Subject: [PATCH 5/8] Load item name in on init --- .../clarin-ref-citation/clarin-ref-citation.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index 916dee89b15..00a9a785484 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -40,7 +40,6 @@ export class ClarinRefCitationComponent implements OnInit { */ itemNameText: string; - /** * The content of the reference box, which will be displayed in the tooltip. * This content is fetched from the RefBox Controller. @@ -73,6 +72,7 @@ export class ClarinRefCitationComponent implements OnInit { console.error('Failed to fetch refbox content:', error); this.refboxContent.next(this.EMPTY_CONTENT); }); + this.itemNameText = this.item?.firstMetadataValue('dc.title'); } /** From 1f07379fbe49e1117a2b530150257bdcdb009d6c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 13:24:20 +0200 Subject: [PATCH 6/8] Secure the refbox content using SafeHtml --- .../clarin-ref-citation.component.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index 00a9a785484..7193e3f09d3 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -10,7 +10,7 @@ import { RequestService } from '../../core/data/request.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { BehaviorSubject } from 'rxjs'; -import { DomSanitizer } from '@angular/platform-browser'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; /** @@ -44,7 +44,12 @@ export class ClarinRefCitationComponent implements OnInit { * The content of the reference box, which will be displayed in the tooltip. * This content is fetched from the RefBox Controller. */ - refboxContent: BehaviorSubject = new BehaviorSubject(null); + refboxContent: BehaviorSubject = new BehaviorSubject(null); + + /** + * The raw content of the reference box, which is fetched from the RefBox Controller. + */ + refBoxCopyContent = ''; /** * The text to be displayed when the ref box content is empty or cannot be fetched. @@ -66,11 +71,12 @@ export class ClarinRefCitationComponent implements OnInit { ngOnInit(): void { void this.fetchRefBoxContent() .then((content) => { - // Sanitize the content to prevent XSS attacks + this.refBoxCopyContent = content; // Store raw HTML this.refboxContent.next(this.sanitizer.bypassSecurityTrustHtml(content)); }).catch((error) => { - console.error('Failed to fetch refbox content:', error); - this.refboxContent.next(this.EMPTY_CONTENT); + console.error('Failed to fetch refbox content:', error); + this.refBoxCopyContent = this.EMPTY_CONTENT; + this.refboxContent.next(this.EMPTY_CONTENT); }); this.itemNameText = this.item?.firstMetadataValue('dc.title'); } @@ -80,11 +86,10 @@ export class ClarinRefCitationComponent implements OnInit { * Remove the html tags from the text and copy only the plain text. */ copyText() { - const displayText = this.refboxContent.value; let plainText = this.EMPTY_CONTENT; - if (displayText) { + if (this.refBoxCopyContent) { const tempDiv = document.createElement('div'); - tempDiv.innerHTML = displayText; + tempDiv.innerHTML = this.refBoxCopyContent; plainText = tempDiv.textContent || ''; } this.clipboard.copy(plainText); From 3efef3346916ec41583e6a291de7c3b66cf29e60 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 13:25:57 +0200 Subject: [PATCH 7/8] Make refbox variable name consistent --- .../clarin-ref-citation.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index 7193e3f09d3..c1c87a82381 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -49,7 +49,7 @@ export class ClarinRefCitationComponent implements OnInit { /** * The raw content of the reference box, which is fetched from the RefBox Controller. */ - refBoxCopyContent = ''; + refboxCopyContent = ''; /** * The text to be displayed when the ref box content is empty or cannot be fetched. @@ -71,11 +71,11 @@ export class ClarinRefCitationComponent implements OnInit { ngOnInit(): void { void this.fetchRefBoxContent() .then((content) => { - this.refBoxCopyContent = content; // Store raw HTML + this.refboxCopyContent = content; // Store raw HTML this.refboxContent.next(this.sanitizer.bypassSecurityTrustHtml(content)); }).catch((error) => { console.error('Failed to fetch refbox content:', error); - this.refBoxCopyContent = this.EMPTY_CONTENT; + this.refboxCopyContent = this.EMPTY_CONTENT; this.refboxContent.next(this.EMPTY_CONTENT); }); this.itemNameText = this.item?.firstMetadataValue('dc.title'); @@ -87,9 +87,9 @@ export class ClarinRefCitationComponent implements OnInit { */ copyText() { let plainText = this.EMPTY_CONTENT; - if (this.refBoxCopyContent) { + if (this.refboxCopyContent) { const tempDiv = document.createElement('div'); - tempDiv.innerHTML = this.refBoxCopyContent; + tempDiv.innerHTML = this.refboxCopyContent; plainText = tempDiv.textContent || ''; } this.clipboard.copy(plainText); From 7c560612d0728eeff1e55a594e86b8d9d27664d3 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 22 Jul 2025 13:59:18 +0200 Subject: [PATCH 8/8] Refactor fetching refbox string from the SafeHtml --- .../clarin-ref-citation/clarin-ref-citation.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index c1c87a82381..94980315738 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -88,9 +88,9 @@ export class ClarinRefCitationComponent implements OnInit { copyText() { let plainText = this.EMPTY_CONTENT; if (this.refboxCopyContent) { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = this.refboxCopyContent; - plainText = tempDiv.textContent || ''; + const parser = new DOMParser(); + const doc = parser.parseFromString(this.refboxCopyContent, 'text/html'); + plainText = doc.body.textContent || ''; } this.clipboard.copy(plainText); setTimeout(() => {