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(HTML Node): Escape data path value in JSON Property #8441

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
222 changes: 131 additions & 91 deletions packages/nodes-base/nodes/Html/Html.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import type {
INodeType,
INodeTypeDescription,
IDataObject,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { placeholder } from './placeholder';
import { getValue } from './utils';
import type { IValueData } from './types';
import { getResolvables } from '@utils/utilities';
import { getResolvables, sanitazeDataPathKey } from '@utils/utilities';

import get from 'lodash/get';

export const capitalizeHeader = (header: string, capitalize?: boolean) => {
if (!capitalize) return header;
Expand All @@ -21,13 +24,97 @@ export const capitalizeHeader = (header: string, capitalize?: boolean) => {
.join(' ');
};

const extractionValuesCollection: INodeProperties = {
displayName: 'Extraction Values',
name: 'extractionValues',
placeholder: 'Add Value',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key under which the extracted value should be saved',
},
{
displayName: 'CSS Selector',
name: 'cssSelector',
type: 'string',
default: '',
placeholder: '.price',
description: 'The CSS selector to use',
},
{
displayName: 'Return Value',
name: 'returnValue',
type: 'options',
options: [
{
name: 'Attribute',
value: 'attribute',
description: 'Get an attribute value like "class" from an element',
},
{
name: 'HTML',
value: 'html',
description: 'Get the HTML the element contains',
},
{
name: 'Text',
value: 'text',
description: 'Get only the text content of the element',
},
{
name: 'Value',
value: 'value',
description: 'Get value of an input, select or textarea',
},
],
default: 'text',
description: 'What kind of data should be returned',
},
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
displayOptions: {
show: {
returnValue: ['attribute'],
},
},
default: '',
placeholder: 'class',
description: 'The name of the attribute to return the value off',
},
{
displayName: 'Return Array',
name: 'returnArray',
type: 'boolean',
default: false,
description:
'Whether to return the values as an array so if multiple ones get found they also get returned separately. If not set all will be returned as a single string.',
},
],
},
],
};

export class Html implements INodeType {
description: INodeTypeDescription = {
displayName: 'HTML',
name: 'html',
icon: 'file:html.svg',
group: ['transform'],
version: 1,
version: [1, 1.1],
subtitle: '={{ $parameter["operation"] }}',
description: 'Work with HTML',
defaults: {
Expand Down Expand Up @@ -143,94 +230,33 @@ export class Html implements INodeType {
'Name of the JSON property in which the HTML to extract the data from can be found. The property can either contain a string or an array of strings.',
},
{
displayName: 'Extraction Values',
name: 'extractionValues',
placeholder: 'Add Value',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
...extractionValuesCollection,
displayOptions: {
show: {
operation: ['extractHtmlContent'],
'@version': [1],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The key under which the extracted value should be saved',
},
{
displayName: 'CSS Selector',
name: 'cssSelector',
type: 'string',
default: '',
placeholder: '.price',
description: 'The CSS selector to use',
},
{
displayName: 'Return Value',
name: 'returnValue',
type: 'options',
options: [
{
name: 'Attribute',
value: 'attribute',
description: 'Get an attribute value like "class" from an element',
},
{
name: 'HTML',
value: 'html',
description: 'Get the HTML the element contains',
},
{
name: 'Text',
value: 'text',
description: 'Get only the text content of the element',
},
{
name: 'Value',
value: 'value',
description: 'Get value of an input, select or textarea',
},
],
default: 'text',
description: 'What kind of data should be returned',
},
{
displayName: 'Attribute',
name: 'attribute',
type: 'string',
displayOptions: {
show: {
returnValue: ['attribute'],
},
},
default: '',
placeholder: 'class',
description: 'The name of the attribute to return the value off',
},
{
displayName: 'Return Array',
name: 'returnArray',
type: 'boolean',
default: false,
description:
'Whether to return the values as an array so if multiple ones get found they also get returned separately. If not set all will be returned as a single string.',
},
],
},
{
...extractionValuesCollection,
default: {
values: [
{
key: '',
cssSelector: '',
returnValue: 'text',
returnArray: false,
},
],
},
displayOptions: {
show: {
operation: ['extractHtmlContent'],
'@version': [{ _cnd: { gt: 1 } }],
},
],
},
},

{
displayName: 'Options',
name: 'options',
Expand Down Expand Up @@ -329,6 +355,7 @@ export class Html implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion;

if (operation === 'convertToHtmlTable' && items.length) {
let table = '';
Expand Down Expand Up @@ -467,14 +494,27 @@ export class Html implements INodeType {

let htmlArray: string[] | string = [];
if (sourceData === 'json') {
if (item.json[dataPropertyName] === undefined) {
throw new NodeOperationError(
this.getNode(),
`No property named "${dataPropertyName}" exists!`,
{ itemIndex },
);
if (nodeVersion === 1) {
const key = sanitazeDataPathKey(item.json, dataPropertyName);
if (item.json[key] === undefined) {
throw new NodeOperationError(
this.getNode(),
`No property named "${dataPropertyName}" exists!`,
{ itemIndex },
);
}
htmlArray = item.json[key] as string;
} else {
const value = get(item.json, dataPropertyName);
if (value === undefined) {
throw new NodeOperationError(
this.getNode(),
`No property named "${dataPropertyName}" exists!`,
{ itemIndex },
);
}
htmlArray = value as string;
}
htmlArray = item.json[dataPropertyName] as string;
} else {
this.helpers.assertBinaryData(itemIndex, dataPropertyName);
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(
Expand All @@ -489,7 +529,7 @@ export class Html implements INodeType {
htmlArray = [htmlArray];
}

for (const html of htmlArray as string[]) {
for (const html of htmlArray) {
const $ = cheerio.load(html);

const newItem: INodeExecutionData = {
Expand Down
21 changes: 4 additions & 17 deletions packages/nodes-base/nodes/Set/v2/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import set from 'lodash/set';
import get from 'lodash/get';
import unset from 'lodash/unset';

import { getResolvables } from '../../../../utils/utilities';
import { getResolvables, sanitazeDataPathKey } from '../../../../utils/utilities';
import type { SetNodeOptions, SetField } from './interfaces';
import { INCLUDE } from './interfaces';

Expand All @@ -35,28 +35,15 @@ const configureFieldHelper = (dotNotation?: boolean) => {
},
};
} else {
const sanitazeKey = (item: IDataObject, key: string) => {
if (item[key] !== undefined) {
return key;
}

if (key.startsWith("['") && key.endsWith("']")) {
key = key.slice(2, -2);
if (item[key] !== undefined) {
return key;
}
}
return key;
};
return {
set: (item: IDataObject, key: string, value: IDataObject) => {
item[sanitazeKey(item, key)] = value;
item[sanitazeDataPathKey(item, key)] = value;
},
get: (item: IDataObject, key: string) => {
return item[sanitazeKey(item, key)];
return item[sanitazeDataPathKey(item, key)];
},
unset: (item: IDataObject, key: string) => {
delete item[sanitazeKey(item, key)];
delete item[sanitazeDataPathKey(item, key)];
},
};
}
Expand Down
17 changes: 17 additions & 0 deletions packages/nodes-base/utils/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,20 @@ export function preparePairedItemDataArray(
if (Array.isArray(pairedItem)) return pairedItem;
return [pairedItem];
}

export const sanitazeDataPathKey = (item: IDataObject, key: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: sanitize

if (item[key] !== undefined) {
return key;
}

if (
(key.startsWith("['") && key.endsWith("']")) ||
(key.startsWith('["') && key.endsWith('"]'))
) {
key = key.slice(2, -2);
if (item[key] !== undefined) {
return key;
}
}
return key;
};
Loading