first commit
This commit is contained in:
221
node_modules/@n8n/eslint-plugin-community-nodes/dist/utils/file-utils.js
generated
vendored
Normal file
221
node_modules/@n8n/eslint-plugin-community-nodes/dist/utils/file-utils.js
generated
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
import { parse, simpleTraverse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { dirname, parse as parsePath } from 'node:path';
|
||||
import { isCredentialTypeClass, isNodeTypeClass, findClassProperty, getStringLiteralValue, findArrayLiteralProperty, extractCredentialInfoFromArray, findSimilarStrings, } from './ast-utils.js';
|
||||
/**
|
||||
* Checks if the given childPath is contained within the parentPath. Resolves
|
||||
* the paths before comparing them, so that relative paths are also supported.
|
||||
*/
|
||||
export function isContainedWithin(parentPath, childPath) {
|
||||
parentPath = path.resolve(parentPath);
|
||||
childPath = path.resolve(childPath);
|
||||
if (parentPath === childPath) {
|
||||
return true;
|
||||
}
|
||||
return childPath.startsWith(parentPath + path.sep);
|
||||
}
|
||||
/**
|
||||
* Joins the given paths to the parentPath, ensuring that the resulting path
|
||||
* is still contained within the parentPath. If not, it throws an error to
|
||||
* prevent path traversal vulnerabilities.
|
||||
*
|
||||
* @throws {UnexpectedError} If the resulting path is not contained within the parentPath.
|
||||
*/
|
||||
export function safeJoinPath(parentPath, ...paths) {
|
||||
const candidate = path.join(parentPath, ...paths);
|
||||
if (!isContainedWithin(parentPath, candidate)) {
|
||||
throw new Error(`Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
export function findPackageJson(startPath) {
|
||||
let currentDir = path.dirname(startPath);
|
||||
while (parsePath(currentDir).dir !== parsePath(currentDir).root) {
|
||||
const testPath = safeJoinPath(currentDir, 'package.json');
|
||||
if (fileExistsWithCaseSync(testPath)) {
|
||||
return testPath;
|
||||
}
|
||||
currentDir = dirname(currentDir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isValidPackageJson(obj) {
|
||||
return typeof obj === 'object' && obj !== null;
|
||||
}
|
||||
function readPackageJsonN8n(packageJsonPath) {
|
||||
try {
|
||||
const content = readFileSync(packageJsonPath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
if (isValidPackageJson(parsed)) {
|
||||
return parsed.n8n ?? {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function resolveN8nFilePaths(packageJsonPath, filePaths) {
|
||||
const packageDir = dirname(packageJsonPath);
|
||||
const resolvedFiles = [];
|
||||
for (const filePath of filePaths) {
|
||||
const sourcePath = filePath.replace(/^dist\//, '').replace(/\.js$/, '.ts');
|
||||
const fullSourcePath = safeJoinPath(packageDir, sourcePath);
|
||||
if (existsSync(fullSourcePath)) {
|
||||
resolvedFiles.push(fullSourcePath);
|
||||
}
|
||||
}
|
||||
return resolvedFiles;
|
||||
}
|
||||
export function readPackageJsonCredentials(packageJsonPath) {
|
||||
const n8nConfig = readPackageJsonN8n(packageJsonPath);
|
||||
const credentialPaths = n8nConfig.credentials ?? [];
|
||||
const credentialFiles = resolveN8nFilePaths(packageJsonPath, credentialPaths);
|
||||
const credentialNames = [];
|
||||
for (const credentialFile of credentialFiles) {
|
||||
try {
|
||||
const credentialName = extractCredentialNameFromFile(credentialFile);
|
||||
if (credentialName) {
|
||||
credentialNames.push(credentialName);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Silently continue if file can't be parsed
|
||||
}
|
||||
}
|
||||
return new Set(credentialNames);
|
||||
}
|
||||
export function extractCredentialNameFromFile(credentialFilePath) {
|
||||
try {
|
||||
const sourceCode = readFileSync(credentialFilePath, 'utf8');
|
||||
const ast = parse(sourceCode, {
|
||||
jsx: false,
|
||||
range: true,
|
||||
});
|
||||
let credentialName = null;
|
||||
simpleTraverse(ast, {
|
||||
enter(node) {
|
||||
if (node.type === AST_NODE_TYPES.ClassDeclaration && isCredentialTypeClass(node)) {
|
||||
const nameProperty = findClassProperty(node, 'name');
|
||||
if (nameProperty) {
|
||||
const nameValue = getStringLiteralValue(nameProperty.value);
|
||||
if (nameValue) {
|
||||
credentialName = nameValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return credentialName;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function validateIconPath(iconPath, baseDir) {
|
||||
const isFile = iconPath.startsWith('file:');
|
||||
const relativePath = iconPath.replace(/^file:/, '');
|
||||
const isSvg = relativePath.endsWith('.svg');
|
||||
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
|
||||
const fullPath = path.join(baseDir, relativePath);
|
||||
const exists = fileExistsWithCaseSync(fullPath);
|
||||
return {
|
||||
isValid: isFile && isSvg && exists,
|
||||
isFile,
|
||||
isSvg,
|
||||
exists,
|
||||
};
|
||||
}
|
||||
export function readPackageJsonNodes(packageJsonPath) {
|
||||
const n8nConfig = readPackageJsonN8n(packageJsonPath);
|
||||
const nodePaths = n8nConfig.nodes ?? [];
|
||||
return resolveN8nFilePaths(packageJsonPath, nodePaths);
|
||||
}
|
||||
export function areAllCredentialUsagesTestedByNodes(credentialName, packageDir) {
|
||||
const packageJsonPath = safeJoinPath(packageDir, 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return false;
|
||||
}
|
||||
const nodeFiles = readPackageJsonNodes(packageJsonPath);
|
||||
let hasAnyCredentialUsage = false;
|
||||
for (const nodeFile of nodeFiles) {
|
||||
const result = checkCredentialUsageInFile(nodeFile, credentialName);
|
||||
if (result.hasUsage) {
|
||||
hasAnyCredentialUsage = true;
|
||||
if (!result.allTestedBy) {
|
||||
return false; // Found usage without testedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasAnyCredentialUsage;
|
||||
}
|
||||
function checkCredentialUsageInFile(nodeFile, credentialName) {
|
||||
try {
|
||||
const sourceCode = readFileSync(nodeFile, 'utf8');
|
||||
const ast = parse(sourceCode, { jsx: false, range: true });
|
||||
let hasUsage = false;
|
||||
let allTestedBy = true;
|
||||
simpleTraverse(ast, {
|
||||
enter(node) {
|
||||
if (node.type === AST_NODE_TYPES.ClassDeclaration && isNodeTypeClass(node)) {
|
||||
const descriptionProperty = findClassProperty(node, 'description');
|
||||
if (!descriptionProperty?.value ||
|
||||
descriptionProperty.value.type !== AST_NODE_TYPES.ObjectExpression) {
|
||||
return;
|
||||
}
|
||||
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
|
||||
if (!credentialsArray) {
|
||||
return;
|
||||
}
|
||||
for (const element of credentialsArray.elements) {
|
||||
const credentialInfo = extractCredentialInfoFromArray(element);
|
||||
if (credentialInfo?.name === credentialName) {
|
||||
hasUsage = true;
|
||||
if (!credentialInfo.testedBy) {
|
||||
allTestedBy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return { hasUsage, allTestedBy };
|
||||
}
|
||||
catch {
|
||||
return { hasUsage: false, allTestedBy: true };
|
||||
}
|
||||
}
|
||||
function fileExistsWithCaseSync(filePath) {
|
||||
try {
|
||||
const dir = path.dirname(filePath);
|
||||
const file = path.basename(filePath);
|
||||
const files = new Set(readdirSync(dir));
|
||||
return files.has(file);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export function findSimilarSvgFiles(targetPath, baseDir) {
|
||||
try {
|
||||
const targetFileName = path.basename(targetPath, path.extname(targetPath));
|
||||
const targetDir = path.dirname(targetPath);
|
||||
// Should not use safeJoinPath here because iconPath can be outside of the node class folder
|
||||
const searchDir = path.join(baseDir, targetDir);
|
||||
if (!existsSync(searchDir)) {
|
||||
return [];
|
||||
}
|
||||
const files = readdirSync(searchDir);
|
||||
const svgFileNames = files
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.map((file) => path.basename(file, '.svg'));
|
||||
const candidateNames = new Set(svgFileNames);
|
||||
const similarNames = findSimilarStrings(targetFileName, candidateNames);
|
||||
return similarNames.map((name) => path.join(targetDir, `${name}.svg`));
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=file-utils.js.map
|
||||
Reference in New Issue
Block a user