diff --git a/src/main/java/com/cflint/plugins/core/ComponentPath.java b/src/main/java/com/cflint/plugins/core/ComponentPath.java new file mode 100644 index 000000000..5104c131d --- /dev/null +++ b/src/main/java/com/cflint/plugins/core/ComponentPath.java @@ -0,0 +1,112 @@ +package com.cflint.plugins.core; + +import org.apache.tools.ant.taskdefs.Java; + +import java.awt.*; +import java.io.File; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.io.IOException; + +public class ComponentPath { + private static HashMap> componentsMapCache; + private Path rootPath; + private static ComponentPath single_instance ; + private String separator; + private static HashSet CFknownScriptFunctions; + + public static ComponentPath getInstance(String folder) { + if (single_instance == null) + single_instance = new ComponentPath(folder); + + return single_instance; + } + + private ComponentPath(String folderPath) { + //work around to find web root + // initialize known sub folders of web root + //without this, to find the web root , we have to search for server/web.xml coldfusion configuration file + String[] knownRootSubDirs = {"api", "voucherengine", "components", "avisfuelcard", "commandchain", "ioc", "parrot", "paycard", "santam", "voucherpos", "access", "customTags", "common"}; + String strlist = System.getProperty("knownsubdirs"); + if (strlist != null && !strlist.isEmpty()) { + knownRootSubDirs = strlist.split(","); + } + HashSet knownRootSubs = new HashSet<>(Arrays.asList(knownRootSubDirs)); + File current = Paths.get(folderPath).toAbsolutePath().normalize().toFile(); + boolean foundRoot = false; + System.out.println("searching web root from " + current.toString()); + do { + String currentDir = current.getParentFile().getName(); + if (knownRootSubs.contains(currentDir.toLowerCase())) { + rootPath = current.getParentFile().getParentFile().toPath(); + foundRoot = true; + System.out.println("found web root" + rootPath.toString()); + } + current = current.getParentFile(); + } while (!foundRoot && current.toPath().getNameCount() != 0); + + componentsMapCache = new HashMap<>(); + separator = System.getProperty("file.separator"); + + String[] knownScriptFuncs = {"ftp", "http", "mail", "pdf", "query", "storedproc", "dbinfo", "imap", "pop", "ldap", "feed"}; + CFknownScriptFunctions = new HashSet<>(Arrays.asList(knownScriptFuncs)); + + } + + public boolean ComponentExists(String componentName, String currentSrcFile) { + + if (isKnownCFScriptFunc(componentName)) return true; + if (rootPath == null) { + System.out.println("root path not found"); + } else { + Path compPath = getComponentPath(componentName, currentSrcFile); + HashSet currentDirFiles = initOrGetFileNamesFromCache(compPath); + if (currentDirFiles.contains(compPath.toAbsolutePath().toString())) { + return true; + } + } + return false; + } + + public boolean isKnownCFScriptFunc(String componentName) { + if (CFknownScriptFunctions.contains(componentName.toLowerCase())) { + return true; + } + return false; + } + + private Path getComponentPath(String componentName, String currentSrcFile) { + String folderpath = ""; + if (componentName.contains(".")) { + folderpath = rootPath.toAbsolutePath() + separator + String.join(separator, componentName.split("\\.")) + ".cfc"; + } else { + Path parentDir = Paths.get(currentSrcFile).getParent(); + folderpath = parentDir.toAbsolutePath().normalize().toString() + separator + componentName + ".cfc"; + } + return Paths.get(folderpath); + + } + + private HashSet initOrGetFileNamesFromCache(Path current) { + File componentFile = current.toAbsolutePath().toFile(); + if (componentFile.isFile()) { + current = current.getParent(); + } + if (!componentsMapCache.containsKey(current)) { + componentsMapCache.put(current.toString(), new HashSet<>()); + } else { + return componentsMapCache.get(current.toString()); + } + try (DirectoryStream stream = Files.newDirectoryStream(current, "*.{cfc,cfm}")) { + for (Path entry : stream) { + componentsMapCache.get(current.toString()).add(entry.toString()); + } + } catch (IOException ex) { + System.out.println(ex.toString()); + } + return componentsMapCache.get(current.toString()); + } +} diff --git a/src/main/java/com/cflint/plugins/core/ComponentUseChecker.java b/src/main/java/com/cflint/plugins/core/ComponentUseChecker.java new file mode 100644 index 000000000..3dfa7af24 --- /dev/null +++ b/src/main/java/com/cflint/plugins/core/ComponentUseChecker.java @@ -0,0 +1,154 @@ +package com.cflint.plugins.core; + +import cfml.parsing.cfscript.CFExpression; +import cfml.parsing.cfscript.script.*; +import com.cflint.BugList; +import com.cflint.CF; +import com.cflint.plugins.CFLintScannerAdapter; +import com.cflint.plugins.Context; + +import net.htmlparser.jericho.Element; +import ro.fortsoft.pf4j.Extension; + +import java.io.Console; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.HashMap; + +import com.cflint.tools.CFTool; + +@Extension +public class ComponentUseChecker extends CFLintScannerAdapter { + + public enum UsageTypes { + NEW_COMPONENT, + CREATEOBJECT, + ATTRIBUTE, + CFML_TEMPLATE, + DI_GET_INSTANCE, + UNDEFINED + } + + ; + static final HashMap patternsMap; + static final Pattern PascalCasePattern; + + static { + patternsMap = new HashMap<>(); + patternsMap.put(UsageTypes.CFML_TEMPLATE, Pattern.compile("template=\"(.*)\"")); + patternsMap.put(UsageTypes.CREATEOBJECT, Pattern.compile("createObject.*component[\\'\\\"\\,\\s]+[\\'\\\"]([\\w\\d.]*)[\\'\\\"]\\s*\\)")); + patternsMap.put(UsageTypes.ATTRIBUTE, Pattern.compile("[\\'\\\"]([\\w\\.]+)[\\'\\\"]")); + patternsMap.put(UsageTypes.NEW_COMPONENT, Pattern.compile("new\\s+([\\w\\d.]*)\\(.*")); + patternsMap.put(UsageTypes.DI_GET_INSTANCE, Pattern.compile("server\\.di\\.getInstance\\([\\s\\'\\\"]([\\w\\d.]*)[\\'\\\"]\\s*\\)")); + PascalCasePattern = Pattern.compile("^[A-Z][a-z]+(?:[A-Z][a-z]+)*$"); + } + + @Override + public void expression(final CFScriptStatement expression, final Context context, final BugList bugs) { + if (expression instanceof CFExpressionStatement) { + final String code = ((CFExpressionStatement) expression).getExpression().Decompile(0); + final int lineNo = expression.getLine() + context.startLine() - 1; + final int offset = expression.getOffset() + context.offset(); + checkComponentUse(context, code, lineNo, offset); + } else if (expression instanceof CFCompDeclStatement) { + CFCompDeclStatement compDeclStatement = ((CFCompDeclStatement) expression); + Map attributes = CFTool.convertMap(compDeclStatement.getAttributes()); + CFExpression extendsOrImplements = null; + if (attributes.containsKey("extends")) { + extendsOrImplements = attributes.get("extends"); + } else if (attributes.containsKey("implements")) + extendsOrImplements = attributes.get("implements"); + if (extendsOrImplements != null) { + final int lineNo = extendsOrImplements.getLine() + context.startLine() - 1; + final int offset = compDeclStatement.getOffset() + context.offset(); + checkComponentUse(context, extendsOrImplements.Decompile(0), lineNo, offset, UsageTypes.ATTRIBUTE); + } + } else if (expression instanceof CFPropertyStatement) { + CFPropertyStatement propStatement = (CFPropertyStatement) expression; + Map attributes = CFTool.convertMap(propStatement.getAttributes()); + CFExpression injectAttribute = null; + if (attributes.containsKey("inject")) { + injectAttribute = attributes.get("inject"); + } + if (injectAttribute != null) { + final int lineNo = injectAttribute.getLine() + context.startLine() - 1; + final int offset = injectAttribute.getOffset() + context.offset(); + checkComponentUse(context, injectAttribute.Decompile(0), lineNo, offset, UsageTypes.ATTRIBUTE); + } + } + + } + + @Override + public void element(final Element element, final Context context, final BugList bugs) { + if (element.getName().equals(CF.CFCOMPONENT)) { + final String attributeVal = element.getAttributeValue("extends"); + if (attributeVal != null && !attributeVal.trim().isEmpty()) { + final int lineNo = element.getSource().getRow(element.getBegin()); + verifyComponentUsage(context, lineNo, element.getBegin(), attributeVal); + } + } else if (element.getName().equals(CF.CFSET)) { + final String content = element.getStartTag().getTagContent().toString(); + final int lineNo = element.getSource().getRow(element.getBegin()); + checkComponentUse(context, content, lineNo, element.getBegin()); + } + } + + + private void checkComponentUse(Context context, String code, int lineNo, int offset) { + UsageTypes componentUsage = getComponentUseIfAny(code); + if (patternsMap.containsKey(componentUsage)) { + checkComponentUse(context, code, lineNo, offset, componentUsage); + } + } + + private void checkComponentUse(Context context, String code, int lineNo, int offset, UsageTypes componentUsage) { + Matcher matcher = patternsMap.get(componentUsage).matcher(code.replace("\n", "").replace("\r", "")); + if (matcher.find()) { + String componentName = matcher.group(1); + verifyComponentUsage(context, lineNo, offset, componentName); + } + } + + private void verifyComponentUsage(Context context, int lineNo, int offset, String componentName) { + String fileName = context.getFilename(); + System.out.println("checking " + fileName); + System.out.println("looking for component: " + componentName); + ComponentPath compPath = ComponentPath.getInstance(fileName); + if (!compPath.ComponentExists(componentName, fileName)) { + context.addMessage("COMPONENT_NOT_FOUND", componentName, lineNo, offset); + } else { + if (componentName.contains(".")) { + String[] paths = componentName.split("\\."); + componentName = paths[paths.length - 1]; + } + if (!PascalCasePattern.matcher(componentName).matches()) { + context.addMessage("INVALID_COMPONENT_USAGE", componentName, lineNo, offset); + } + } + + } + + protected UsageTypes getComponentUseIfAny(final String code) { + String codeLine = code.replaceAll("\\s+", "").toLowerCase(); + if (codeLine.contains("createobject")) { + return UsageTypes.CREATEOBJECT; + } else if (codeLine.contains("new")) { + return UsageTypes.NEW_COMPONENT; + } else if (codeLine.contains("server.di.getinstance")) { + return UsageTypes.DI_GET_INSTANCE; + } else if (codeLine.contains("extends")) { + return UsageTypes.ATTRIBUTE; + } else if (codeLine.contains("implements")) { + return UsageTypes.ATTRIBUTE; + } else if (codeLine.contains("inject")) { + return UsageTypes.ATTRIBUTE; + } else if (codeLine.contains("template")) { + return UsageTypes.CFML_TEMPLATE; + } else { + return UsageTypes.UNDEFINED; + } + } + +} diff --git a/src/main/resources/cflint.definition.json b/src/main/resources/cflint.definition.json index 2277e16f0..3e6a55d35 100644 --- a/src/main/resources/cflint.definition.json +++ b/src/main/resources/cflint.definition.json @@ -1,5 +1,27 @@ { "ruleImpl": [ + { + "name": "ComponentUseChecker", + "className": "ComponentUseChecker", + "message": [ + { + "code": "INVALID_COMPONENT_USAGE", + "severity": "ERROR", + "messageText": "Component '${variable}' usage is not valid . Please use ${case}" + }, + { + "code": "COMPONENT_NOT_FOUND", + "severity": "INFO", + "messageText": "Component '${variable}' not found." + } + ], + "parameter": [ + { + "name": "case", + "value": "PascalCase" + } + ] + }, { "name": "ArgDefChecker", "message": [ diff --git a/src/test/java/com/cflint/TestCFBugs_ComponentUse.java b/src/test/java/com/cflint/TestCFBugs_ComponentUse.java new file mode 100644 index 000000000..d367f54af --- /dev/null +++ b/src/test/java/com/cflint/TestCFBugs_ComponentUse.java @@ -0,0 +1,218 @@ +package com.cflint; + +import static org.junit.Assert.assertEquals; + +import java.util.Collection; +import java.util.List; + +import com.cflint.plugins.core.ComponentPath; +import org.junit.Before; +import org.junit.Test; + +import com.cflint.api.CFLintAPI; +import com.cflint.api.CFLintResult; +import com.cflint.config.ConfigBuilder; +import com.cflint.exception.CFLintScanException; + +public class TestCFBugs_ComponentUse { + + private CFLintAPI cfBugs; + private String basePath = "src/test/resources/com/cflint/componentusagecheck/"; + + @Before + public void setUp() throws Exception { + final ConfigBuilder configBuilder = new ConfigBuilder().include("INVALID_COMPONENT_USAGE"); + cfBugs = new CFLintAPI(configBuilder.build()); + ComponentPath rootPath = ComponentPath.getInstance(basePath + "api/ura/Application.cfc"); + } + + @Test + public void testNewComponentError() throws CFLintScanException { + final String tagSrc = "config = new components.configuration.handler('VoucherEngine', Attributes);"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "ComponentName.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testNewComponentNoError() throws CFLintScanException { + final String tagSrc = "config = new components.configuration.Manager('VoucherEngine', Attributes);"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "ComponentName.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testExtendsComponentError() throws CFLintScanException { + final String tagSrc = "component extends = 'base' restpath = 'authenticate' rest = true {\n" + + " public function init() {\n" + + " return this;\n" + + " }\n" + + "}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, basePath + "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testExtendsComponentNoError() throws CFLintScanException { + final String tagSrc = "component extends = 'api.Base' restpath = 'authenticate' rest = true {\n" + + " public function init() {\n" + + " return this;\n" + + " }\n" + + "}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + + @Test + public void testImplementsComponentError() throws CFLintScanException { + final String tagSrc = "component implements = 'base' restpath = 'authenticate' rest = true {\n" + + " public function init() {\n" + + " return this;\n" + + " }\n" + + "}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, basePath + "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testImplementsComponentNoError() throws CFLintScanException { + final String tagSrc = "component implements = 'api.Base' restpath = 'authenticate' rest = true {\n" + + " public function init() {\n" + + " return this;\n" + + " }\n" + + "}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testImplementsComponentErrorWithFolderPath() throws CFLintScanException { + final String tagSrc = "component implements = 'components.configuration.handler' {}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testImplementsComponentNoErrorWithFolderPath() throws CFLintScanException { + final String tagSrc = "component implements = 'components.configuration.Manager' {}"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testCreateObjectError() throws CFLintScanException { + final String tagSrc = "server.di = createObject('component', 'ioc.di').init();"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testCreateObjectNoError() throws CFLintScanException { + final String tagSrc = "server.di = createObject('component', 'components.XmlRpc').init();"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + + @Test + public void testPropertyInjectError() throws CFLintScanException { + final String tagSrc = "property name = 'reporter' inject = 'components.configuration.handler';"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testPropertyInjectNoError() throws CFLintScanException { + final String tagSrc = "property name = 'reporter' inject = 'components.configuration.Manager';"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testExtendsCFMLComponentError() throws CFLintScanException { + final String tagSrc = "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testExtendsCFMLComponentNoError() throws CFLintScanException { + final String tagSrc = "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testCFMLCreateObjectError() throws CFLintScanException { + final String tagSrc = "\n" + + "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testCFMLCreateObjectNoError() throws CFLintScanException { + final String tagSrc = "\n" + + "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testCFMLNewComponentError() throws CFLintScanException { + final String tagSrc = "\n" + + "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testCFMLNewComponentNoError() throws CFLintScanException { + final String tagSrc = "\n" + + "\n" + + ""; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + @Test + public void testDiGetInstanceError() throws CFLintScanException { + final String tagSrc = "exporter = server.di.getInstance('components.configuration.handler');"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 1, result.size()); + } + + @Test + public void testDiGetInstanceNoError() throws CFLintScanException { + final String tagSrc = "exporter = server.di.getInstance('components.configuration.Manager');"; + CFLintResult lintresult = cfBugs.scan(tagSrc, "NicComponentNm.cfc"); + Collection> result = lintresult.getIssues().values(); + assertEquals(lintresult.getIssues().values().toString(), 0, result.size()); + } + + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/api/Base.cfc b/src/test/resources/com/cflint/componentusagecheck/api/Base.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/api/Base.cfc @@ -0,0 +1,3 @@ +component { + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/api/ura/Application.cfc b/src/test/resources/com/cflint/componentusagecheck/api/ura/Application.cfc new file mode 100644 index 000000000..4843350b5 --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/api/ura/Application.cfc @@ -0,0 +1,9 @@ +component extends = 'base' { + property name = 'reporter' inject = 'components.error.Errorfunctions'; + public function init() { + var config = new components.configuration.Manager('VoucherEngine', Attributes); + server.di = createObject('component', 'components.XmlRpc').init(); + exporter = server.di.getInstance('components.eftBatchProcessor.service.SecExporter'); + return this; + } +} \ No newline at end of file diff --git a/src/test/resources/com/cflint/componentusagecheck/base.cfc b/src/test/resources/com/cflint/componentusagecheck/base.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/base.cfc @@ -0,0 +1,3 @@ +component { + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/components/XmlRpc.cfc b/src/test/resources/com/cflint/componentusagecheck/components/XmlRpc.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/components/XmlRpc.cfc @@ -0,0 +1,3 @@ +component { + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/components/configuration/Manager.cfc b/src/test/resources/com/cflint/componentusagecheck/components/configuration/Manager.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/components/configuration/Manager.cfc @@ -0,0 +1,3 @@ +component { + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/components/configuration/handler.cfc b/src/test/resources/com/cflint/componentusagecheck/components/configuration/handler.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/components/configuration/handler.cfc @@ -0,0 +1,3 @@ +component { + +} diff --git a/src/test/resources/com/cflint/componentusagecheck/ioc/di.cfc b/src/test/resources/com/cflint/componentusagecheck/ioc/di.cfc new file mode 100644 index 000000000..cfb3d3dec --- /dev/null +++ b/src/test/resources/com/cflint/componentusagecheck/ioc/di.cfc @@ -0,0 +1,3 @@ +component { + +}