Skip to content

Commit

Permalink
feat(fe:FSADT1-1565): Highlight text on Auto Complete options (#1286)
Browse files Browse the repository at this point in the history
* Changed to v-dompurify-html as using v-html can expose the application to XSS (cross-site scripting) attacks

* Changed to v-dompurify-html as using v-html can expose the application to XSS (cross-site scripting) attacks

* Changed to v-dompurify-html as using v-html can expose the application to XSS (cross-site scripting) attacks

* feat(fe): Highlight text on Auto Complete options

* chore: add vue-dompurify-html plugin to Cypress

* Safely escaped special characters

---------

Co-authored-by: Fernando Terra <79578735+fterra-encora@users.noreply.github.com>
Co-authored-by: Fernando Terra <fernando.terra@encora.com>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 277c4f9 commit 82759fa
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 13 deletions.
9 changes: 8 additions & 1 deletion frontend/cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import './commands'

import { mount } from 'cypress/vue'
import '@cypress/code-coverage/support'
import VueDOMPurifyHTML from "vue-dompurify-html";
import '@/styles'

declare global {
Expand All @@ -15,4 +16,10 @@ declare global {
}
}

Cypress.Commands.add('mount', mount)
Cypress.Commands.add('mount', (component, options = {}) => {
options.global = options.global || {};
options.global.plugins = options.global.plugins || [];
options.global.plugins.push(VueDOMPurifyHTML);

return mount(component, options);
});
3 changes: 2 additions & 1 deletion frontend/src/components/forms/AutoCompleteInputComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CDSComboBox } from "@carbon/web-components";
import { useEventBus } from "@vueuse/core";
// Types
import type { BusinessSearchResult, CodeNameType, CodeNameValue } from "@/dto/CommonTypesDto";
import { highlightMatch } from "@/services/ForestClientService";
import { isEmpty, type ValidationMessageType } from "@/dto/CommonTypesDto";
import type { DROPDOWN_SIZE } from "@carbon/web-components/es/components/dropdown/defs";
Expand Down Expand Up @@ -363,7 +364,7 @@ const safeHelperText = computed(() => props.tip || " ");
</template>
<template v-else>
<slot :value="item.value">
{{ item.name }}
<span v-dompurify-html="highlightMatch(item.name, inputValue)"></span>
</slot>
</template>
</cds-combo-box-item>
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/helpers/ForestClientUserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ class ForestClientUserSession implements SessionProperties {
}
};



private processName = (
payload: any,
provider: string
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/SearchPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useFetchTo } from "@/composables/useFetch";
import { useEventBus } from "@vueuse/core";
import type { ClientSearchResult, CodeNameValue } from "@/dto/CommonTypesDto";
import { adminEmail, getObfuscatedEmailLink, toTitleCase } from "@/services/ForestClientService";
import { adminEmail, getObfuscatedEmailLink, toTitleCase, highlightMatch } from "@/services/ForestClientService";
import summit from "@carbon/pictograms/es/summit";
import userSearch from "@carbon/pictograms/es/user--search";
import useSvg from "@/composables/useSvg";
Expand Down Expand Up @@ -232,7 +232,7 @@ onMounted(() => {
#="{ value }"
>
<div class="search-result-item" v-if="value">
{{ searchResultToText(value) }}
<span v-dompurify-html="highlightMatch(searchResultToText(value), searchKeyword)"></span>
<cds-tag :type="tagColor(value.clientStatus)" title="">
<span>{{ value.clientStatus }}</span>
</cds-tag>
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/pages/SubmissionReviewPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,11 @@ const rejectValidation = reactive<Record<string, boolean>>({
});
const cleanedRejectionReason = computed(() => {
return data.value.rejectionReason.replace(/<div>&nbsp;<\/div>/g, '').replace(/<p>/g, ' ').replace(/<\/p>/g, '');
return data.value.rejectionReason
? "Client " + data.value.rejectionReason.replace(/<div>&nbsp;<\/div>/g, '').replace(/<p>/g, ' ').replace(/<\/p>/g, '')
: '';
});
const isProcessing = computed(() => {
const processingStatus = (
!data.value.business.clientNumber
Expand All @@ -330,9 +331,6 @@ const isProcessing = computed(() => {
return processingStatus;
});
</script>

<template>
Expand Down Expand Up @@ -577,7 +575,10 @@ const isProcessing = computed(() => {
</read-only-component>

<read-only-component label="Reason for rejection" v-if="data.submissionStatus === 'Rejected'">
<span class="body-compact-01" style="width: 40rem" v-html="'Client' + cleanedRejectionReason"></span>
<span class="body-compact-01"
style="width: 40rem"
v-dompurify-html="cleanedRejectionReason">
</span>
</read-only-component>

</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/staffform/ReviewWizardStep.vue
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ watch([validation], () => {
<span v-if="address.notes &&
address.notes.length"
class="body-compact-01">
<span v-html="getFormattedHtml(address.notes)"></span>
<span v-dompurify-html="getFormattedHtml(address.notes)"></span>
</span>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/services/ForestClientService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,21 @@ export const getObfuscatedEmailLink = email => {
export const getFormattedHtml = ((value: string) => {
return value ? value.replace(/\n/g, '<br>') : '';
});

export const highlightMatch = (itemName: string, searchTerm: string): string => {
const trimmedSearchTerm = searchTerm.trim();
if (!trimmedSearchTerm) return itemName;

// Escape special characters in the search term
const escapedSearchTerm = trimmedSearchTerm.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const regex = new RegExp(`(${escapedSearchTerm})`, 'i');
const parts = itemName.split(regex);

return parts
.map(part =>
part.toLowerCase() === trimmedSearchTerm.toLowerCase()
? `<span>${part}</span>`
: `<strong>${part}</strong>`
)
.join('');
};

0 comments on commit 82759fa

Please sign in to comment.