Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Avoid sanitizing output to search node data #8126

Merged
merged 9 commits into from
Dec 22, 2023
Merged
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
34 changes: 33 additions & 1 deletion cypress/e2e/5-ndv.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,8 @@ describe('NDV', () => {
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
})
});

it('should show node name and version in settings', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);

Expand All @@ -490,6 +491,34 @@ describe('NDV', () => {
ndv.getters.nodeVersion().should('have.text', 'Function node version 1 (Deprecated)');
ndv.actions.close();
});

it('Should render xml and html tags as strings and can search', () => {
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`);

workflowPage.actions.executeWorkflow();

workflowPage.actions.openNode('Edit Fields');

ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table');

ndv.getters.outputTableRow(1).should('include.text', '<?xml version="1.0" encoding="UTF-8"?> <library>');

cy.document().trigger('keyup', { key: '/' });
ndv.getters.searchInput().filter(':focus').type('<lib');

ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib')

ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click();

ndv.getters.outputDataContainer().should('have.text', '[{"body": "<?xml version="1.0" encoding="UTF-8"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"}]');
ndv.getters.outputDataContainer().find('mark').should('have.text', '<lib')

ndv.getters.outputDisplayMode().find('label').eq(2).should('include.text', 'Schema');
ndv.getters.outputDisplayMode().find('label').eq(2).click({force: true});
ndv.getters.outputDataContainer().findChildByTestId('run-data-schema-item').find('> span').should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
});

it('should properly show node execution indicator', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
Expand All @@ -499,6 +528,7 @@ describe('NDV', () => {
ndv.getters.nodeExecuteButton().click();
ndv.getters.nodeRunSuccessIndicator().should('exist');
});

it('should properly show node execution indicator for multiple nodes', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
Expand All @@ -513,6 +543,7 @@ describe('NDV', () => {
workflowPage.actions.openNode('Code');
ndv.getters.nodeRunErrorIndicator().should('exist');
});

it('Should handle mismatched option attributes', () => {
workflowPage.actions.addInitialNodeToCanvas('LDAP', { keepNdvOpen: true, action: 'Create a new entry' });
// Add some attributes in Create operation
Expand All @@ -521,6 +552,7 @@ describe('NDV', () => {
// Attributes should be empty after operation change
cy.getByTestId('parameter-item').contains('Currently no items exist').should('exist');
});

it('Should keep RLC values after operation change', () => {
const TEST_DOC_ID = '1111';
workflowPage.actions.addInitialNodeToCanvas('Google Sheets', { keepNdvOpen: true, action: 'Append row in sheet' });
Expand Down
53 changes: 53 additions & 0 deletions cypress/fixtures/Test_workflow_xml_output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"meta": {
"instanceId": "2d1cf27f75b18bb9e146336f791c37884f4fc7ddb97c2def27c0444d106778bf"
},
"nodes": [
{
"parameters": {},
"id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
420,
220
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "body",
"stringValue": "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"
}
]
},
"options": {}
},
"id": "45888152-7c5f-4d88-9039-660c594da084",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
640,
220
]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}
22 changes: 13 additions & 9 deletions packages/editor-ui/src/components/RunDataJson.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
@update:selectedValue="selectedJsonPath = $event"
>
<template #renderNodeKey="{ node }">
<span
<TextWithHighlights
:content="getContent(node.key)"
:search="search"
data-target="mappable"
:data-value="getJsonParameterPath(node.path)"
:data-name="node.key"
Expand All @@ -43,13 +45,18 @@
[$style.mappable]: mappingEnabled,
[$style.dragged]: draggingPath === node.path,
}"
v-html="highlightSearchTerm(node.key)"
/>
</template>
<template #renderNodeValue="{ node }">
<span v-if="isNaN(node.index)" v-html="highlightSearchTerm(node.content)" />
<span
<TextWithHighlights
v-if="isNaN(node.index)"
:content="getContent(node.content)"
:search="search"
/>
<TextWithHighlights
v-else
:content="getContent(node.content)"
:search="search"
data-target="mappable"
:data-value="getJsonParameterPath(node.path)"
:data-name="getListItemName(node.path)"
Expand All @@ -60,7 +67,6 @@
[$style.dragged]: draggingPath === node.path,
}"
class="ph-no-capture"
v-html="highlightSearchTerm(node.content)"
/>
</template>
</vue-json-pretty>
Expand All @@ -76,7 +82,6 @@ import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
import Draggable from '@/components/Draggable.vue';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { isString } from '@/utils/typeGuards';
import { highlightText, sanitizeHtml } from '@/utils/htmlUtils';
import { shorten } from '@/utils/typesUtils';
import type { INodeUi } from '@/Interface';
import { mapStores } from 'pinia';
Expand All @@ -86,6 +91,7 @@ import { getMappedExpression } from '@/utils/mappingUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { nonExistingJsonPath } from '@/constants';
import { useExternalHooks } from '@/composables/useExternalHooks';
import TextWithHighlights from './TextWithHighlights.vue';

const RunDataJsonActions = defineAsyncComponent(
async () => import('@/components/RunDataJsonActions.vue'),
Expand All @@ -98,6 +104,7 @@ export default defineComponent({
Draggable,
RunDataJsonActions,
MappingPill,
TextWithHighlights,
},
props: {
editMode: {
Expand Down Expand Up @@ -202,9 +209,6 @@ export default defineComponent({
getListItemName(path: string): string {
return path.replace(/^(\["?\d"?]\.?)/g, '');
},
highlightSearchTerm(value: string): string {
return sanitizeHtml(highlightText(this.getContent(value), this.search));
},
},
});
</script>
Expand Down
21 changes: 13 additions & 8 deletions packages/editor-ui/src/components/RunDataSchemaItem.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts" setup>
import { computed } from 'vue';
import type { INodeUi, Schema } from '@/Interface';
import { highlightText, sanitizeHtml } from '@/utils/htmlUtils';
import { checkExhaustive } from '@/utils/typeGuards';
import { shorten } from '@/utils/typesUtils';
import { getMappedExpression } from '@/utils/mappingUtils';
import TextWithHighlights from './TextWithHighlights.vue';

type Props = {
schema: Schema;
Expand All @@ -30,12 +30,8 @@ const isFlat = computed(
props.schema.value.every((v) => !Array.isArray(v.value)),
);
const key = computed((): string | undefined => {
const highlightedKey = sanitizeHtml(highlightText(props.schema.key, props.search));
return isSchemaParentTypeArray.value ? `[${highlightedKey}]` : highlightedKey;
return isSchemaParentTypeArray.value ? `[${props.schema.key}]` : props.schema.key;
});
const parentKey = computed((): string | undefined =>
sanitizeHtml(highlightText(props.parent.key, props.search)),
);
const schemaName = computed(() =>
isSchemaParentTypeArray.value ? `${props.schema.type}[${props.schema.key}]` : props.schema.key,
);
Expand Down Expand Up @@ -99,8 +95,17 @@ const getIconBySchemaType = (type: Schema['type']): string => {
data-target="mappable"
>
<font-awesome-icon :icon="getIconBySchemaType(schema.type)" size="sm" />
<span v-if="isSchemaParentTypeArray" v-html="parentKey" />
<span v-if="key" :class="{ [$style.arrayIndex]: isSchemaParentTypeArray }" v-html="key" />
<TextWithHighlights
v-if="isSchemaParentTypeArray"
:content="props.parent?.key"
:search="props.search"
/>
<TextWithHighlights
v-if="key"
:class="{ [$style.arrayIndex]: isSchemaParentTypeArray }"
:content="key"
:search="props.search"
/>
</span>
</div>
<span v-if="text" :class="$style.text">{{ text }}</span>
Expand Down
22 changes: 12 additions & 10 deletions packages/editor-ui/src/components/RunDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
[$style.draggingHeader]: isDragging,
}"
>
<span v-html="highlightSearchTerm(column || '')" />
<TextWithHighlights
:content="getValueToRender(column || '')"
:search="search"
/>
<div :class="$style.dragButton">
<font-awesome-icon icon="grip-vertical" />
</div>
Expand Down Expand Up @@ -117,10 +120,11 @@
@mouseleave="onMouseLeaveCell"
:class="hasJsonInColumn(index2) ? $style.minColWidth : $style.limitColWidth"
>
<span
<TextWithHighlights
v-if="isSimple(data)"
:content="getValueToRender(data)"
:search="search"
:class="{ [$style.value]: true, [$style.empty]: isEmpty(data) }"
v-html="highlightSearchTerm(data)"
/>
<n8n-tree :nodeClass="$style.nodeClass" v-else :value="data">
<template #label="{ label, path }">
Expand All @@ -141,9 +145,10 @@
>
</template>
<template #value="{ value }">
<span
<TextWithHighlights
:content="getValueToRender(value)"
:search="search"
:class="{ [$style.nestedValue]: true, [$style.empty]: isEmpty(value) }"
v-html="highlightSearchTerm(value)"
/>
</template>
</n8n-tree>
Expand All @@ -162,7 +167,6 @@ import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type { INodeUi, ITableData, NDVState } from '@/Interface';
import { shorten } from '@/utils/typesUtils';
import { highlightText, sanitizeHtml } from '@/utils/htmlUtils';
import { getPairedItemId } from '@/utils/pairedItemUtils';
import type { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow';
import Draggable from './Draggable.vue';
Expand All @@ -171,14 +175,15 @@ import { useNDVStore } from '@/stores/ndv.store';
import MappingPill from './MappingPill.vue';
import { getMappedExpression } from '@/utils/mappingUtils';
import { useExternalHooks } from '@/composables/useExternalHooks';
import TextWithHighlights from './TextWithHighlights.vue';

const MAX_COLUMNS_LIMIT = 40;

type DraggableRef = InstanceType<typeof Draggable>;

export default defineComponent({
name: 'run-data-table',
components: { Draggable, MappingPill },
components: { Draggable, MappingPill, TextWithHighlights },
props: {
node: {
type: Object as PropType<INodeUi>,
Expand Down Expand Up @@ -392,9 +397,6 @@ export default defineComponent({
}
return value;
},
highlightSearchTerm(value: string): string {
return sanitizeHtml(highlightText(this.getValueToRender(value), this.search));
},
onDragStart() {
this.draggedColumn = true;
this.ndvStore.resetMappingTelemetry();
Expand Down
52 changes: 52 additions & 0 deletions packages/editor-ui/src/components/TextWithHighlights.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { GenericValue } from 'n8n-workflow';
import { computed } from 'vue';

const props = defineProps({
content: {
type: [Object, String, Number] as PropType<GenericValue>,
},
search: {
type: String,
},
});

const splitTextBySearch = (
text = '',
search = '',
): Array<{ tag: 'span' | 'mark'; content: string }> => {
if (!search) {
return [
{
tag: 'span',
content: text,
},
];
}

const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
const pattern = new RegExp(`(${escapeRegExp(search)})`, 'i');
const splitText = text.split(pattern);

return splitText.map((t) => ({ tag: pattern.test(t) ? 'mark' : 'span', content: t }));
mutdmour marked this conversation as resolved.
Show resolved Hide resolved
};

const parts = computed(() => {
return props.search && typeof props.content === 'string'
? splitTextBySearch(props.content, props.search)
: [];
});
</script>

<template>
<span v-if="parts.length && typeof props.content === 'string'">
<template v-for="(part, index) in parts">
<mark v-if="part.tag === 'mark' && part.content" :key="`mark-${index}`">{{
part.content
}}</mark>
<span v-else-if="part.content" :key="`span-${index}`">{{ part.content }}</span>
</template>
</span>
<span v-else>{{ props.content }}</span>
</template>
Loading
Loading