fix: Sharepoint file selection correctly applies filter (#23155)

This commit is contained in:
Dimitri Lavrenük
2025-12-19 17:17:07 +01:00
committed by GitHub
parent 93bb0c5399
commit e0ab5ba45a
4 changed files with 78 additions and 10 deletions
@@ -12,6 +12,8 @@ import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
import type { IErrorResponse } from './interfaces';
import { microsoftSharePointApiRequest } from '../transport';
export const escapeFilterValue = (value: string) => value.replaceAll("'", "''");
export async function simplifyItemPostReceive(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
@@ -6,6 +6,7 @@ import type {
} from 'n8n-workflow';
import type { IDriveItem, IList, IListItem, ISite } from '../helpers/interfaces';
import { escapeFilterValue } from '../helpers/utils';
import { microsoftSharePointApiRequest } from '../transport';
export async function getFiles(
@@ -34,7 +35,7 @@ export async function getFiles(
$select: 'id,name,file',
};
if (filter) {
qs.$filter = `name eq '${filter}'`;
qs.$filter = `startswith(name, '${escapeFilterValue(filter)}')`;
}
response = await microsoftSharePointApiRequest.call(
this,
@@ -85,9 +86,6 @@ export async function getFolders(
// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/filtering-results?view=odsp-graph-online#filterable-properties
$filter: 'folder ne null',
};
if (filter) {
qs.$filter = `name eq '${filter}'`;
}
response = await microsoftSharePointApiRequest.call(
this,
'GET',
@@ -100,7 +98,7 @@ export async function getFolders(
const items: IDriveItem[] = response.value;
const results: INodeListSearchItems[] = items
.filter((x) => x.folder)
.filter((x) => x.folder && (!filter || x.name?.toLowerCase()?.includes?.(filter.toLowerCase())))
.map((g) => ({
name: g.name,
value: g.id,
@@ -137,7 +135,7 @@ export async function getItems(
$select: 'id,fields',
};
if (filter) {
qs.$filter = `fields/Title eq '${filter}'`;
qs.$filter = `fields/Title eq '${escapeFilterValue(filter)}'`;
}
response = await microsoftSharePointApiRequest.call(
this,
@@ -185,7 +183,7 @@ export async function getLists(
$select: 'id,displayName',
};
if (filter) {
qs.$filter = `displayName eq '${filter}'`;
qs.$filter = `displayName eq '${escapeFilterValue(filter)}'`;
}
response = await microsoftSharePointApiRequest.call(
this,
@@ -2,7 +2,7 @@ import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import type { IBinaryData, IExecuteSingleFunctions } from 'n8n-workflow';
import { downloadFilePostReceive } from '../../helpers/utils';
import { downloadFilePostReceive, escapeFilterValue } from '../../helpers/utils';
describe('Microsoft SharePoint Node', () => {
let executeSingleFunctions: MockProxy<IExecuteSingleFunctions>;
@@ -51,4 +51,13 @@ describe('Microsoft SharePoint Node', () => {
},
]);
});
describe('escapeFilterValue', () => {
it('should escape single quotes', () => {
expect(escapeFilterValue("hello' there ''")).toEqual("hello'' there ''''");
});
it('should not escape double quotes', () => {
expect(escapeFilterValue('hello " there ""')).toEqual('hello " there ""');
});
});
});
@@ -75,7 +75,7 @@ describe('Microsoft SharePoint Node', () => {
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
expect(mockRequestWithAuthentication.mock.calls[0][1]).toMatchObject({
qs: {
$filter: "name eq 'file'",
$filter: "startswith(name, 'file')",
},
});
expect(listSearchResult).toEqual({
@@ -88,6 +88,26 @@ describe('Microsoft SharePoint Node', () => {
});
});
it('properly escapes filter parameter', async () => {
const mockResponse = {
'@odata.nextLink':
'https://mydomain.sharepoint.com/_api/v2.0/sites(%27mydomain.sharepoint.com,site1%27)/items?%24skiptoken=aWQ9MjFFQkEzOUMtMkU3My00NzgwLUFBQzEtMTVDNzlDMTk4QjlB',
value: [],
};
mockRequestWithAuthentication.mockReturnValue(mockResponse);
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('site');
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('folder');
await node.methods.listSearch.getFiles.call(loadOptionsFunctions, "fi'le'");
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
expect(mockRequestWithAuthentication.mock.calls[0][1]).toMatchObject({
qs: {
$filter: "startswith(name, 'fi''le''')",
},
});
});
it('should list search files with pagination', async () => {
const mockResponse = {
value: [
@@ -166,7 +186,7 @@ describe('Microsoft SharePoint Node', () => {
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
expect(mockRequestWithAuthentication.mock.calls[0][1]).toMatchObject({
qs: {
$filter: "name eq 'folder'",
$filter: 'folder ne null',
},
});
expect(listSearchResult).toEqual({
@@ -278,6 +298,26 @@ describe('Microsoft SharePoint Node', () => {
});
});
it('properly escapes filter parameter', async () => {
const mockResponse = {
'@odata.nextLink':
'https://mydomain.sharepoint.com/_api/v2.0/sites(%27mydomain.sharepoint.com,site1%27)/listItems?%24skiptoken=aWQ9MjFFQkEzOUMtMkU3My00NzgwLUFBQzEtMTVDNzlDMTk4QjlB',
value: [],
};
mockRequestWithAuthentication.mockReturnValue(mockResponse);
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('site');
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('list');
await node.methods.listSearch.getItems.call(loadOptionsFunctions, "Ti'le'");
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
expect(mockRequestWithAuthentication.mock.calls[0][1]).toMatchObject({
qs: {
$filter: "fields/Title eq 'Ti''le'''",
},
});
});
it('should list search items with pagination', async () => {
const mockResponse = {
value: [
@@ -362,6 +402,25 @@ describe('Microsoft SharePoint Node', () => {
});
});
it('properly escapes filter parameter', async () => {
const mockResponse = {
'@odata.nextLink':
'https://mydomain.sharepoint.com/_api/v2.0/sites(%27mydomain.sharepoint.com,site1%27)/lists?%24skiptoken=aWQ9MjFFQkEzOUMtMkU3My00NzgwLUFBQzEtMTVDNzlDMTk4QjlB',
value: [],
};
mockRequestWithAuthentication.mockReturnValue(mockResponse);
loadOptionsFunctions.getNodeParameter.mockReturnValueOnce('site');
await node.methods.listSearch.getLists.call(loadOptionsFunctions, "' or '1'='1");
expect(mockRequestWithAuthentication).toHaveBeenCalledTimes(1);
expect(mockRequestWithAuthentication.mock.calls[0][1]).toMatchObject({
qs: {
$filter: "displayName eq ''' or ''1''=''1'",
},
});
});
it('should list search lists with pagination', async () => {
const mockResponse = {
value: [