fix(HTML Node): Escape data path value in JSON Property (#8441)

This commit is contained in:
Michael Kret 2024-01-26 11:51:03 +00:00 committed by GitHub
parent 7c49004018
commit fc5c562785
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 152 additions and 108 deletions

View file

@ -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;
@ -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: {
@ -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],
},
},
},
{
...extractionValuesCollection,
default: {
values: [
{
key: '',
cssSelector: '',
returnValue: 'text',
returnArray: false,
},
],
},
displayOptions: {
show: {
operation: ['extractHtmlContent'],
'@version': [{ _cnd: { gt: 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.',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
@ -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 = '';
@ -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(
@ -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 = {

View file

@ -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';
@ -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)];
},
};
}

View file

@ -326,3 +326,20 @@ export function preparePairedItemDataArray(
if (Array.isArray(pairedItem)) return pairedItem;
return [pairedItem];
}
export const sanitazeDataPathKey = (item: IDataObject, key: string) => {
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;
};