diff --git a/build/build.properties b/build/build.properties index 3d2d417a..d60adc40 100644 --- a/build/build.properties +++ b/build/build.properties @@ -10,16 +10,16 @@ java.debug=true #dependencies dependencies.dir=${basedir}/lib -cfml.version=5.3.9.141 +cfml.version=5.3.9.160 cfml.extensions=8D7FB0DF-08BB-1589-FE3975678F07DB17 -cfml.loader.version=2.6.20 +cfml.loader.version=2.7.2 cfml.cli.version=${cfml.loader.version}.${cfml.version} lucee.version=${cfml.version} # Don't bump this version. Need to remove this dependency from cfmlprojects.org lucee.config.version=5.2.4.37 jre.version=jdk-11.0.15+10 launch4j.version=3.14 -runwar.version=4.7.7 +runwar.version=4.7.13 jline.version=3.21.0 jansi.version=2.3.2 jgit.version=5.13.0.202109080827-r diff --git a/build/build.xml b/build/build.xml index b5502e26..f0f6eee0 100644 --- a/build/build.xml +++ b/build/build.xml @@ -16,8 +16,8 @@ External Dependencies: - - + + @@ -382,7 +382,7 @@ External Dependencies: - + @@ -1071,7 +1071,7 @@ External Dependencies: - + diff --git a/src/cfml/system/BaseTask.cfc b/src/cfml/system/BaseTask.cfc index 81ccc4fe..b041d4f8 100644 --- a/src/cfml/system/BaseTask.cfc +++ b/src/cfml/system/BaseTask.cfc @@ -54,6 +54,46 @@ component accessors="true" extends='commandbox.system.BaseCommand' { wirebox.getInstance( 'moduleService' ).registerAndActivateModule( moduleName, invocationPath ); } + /** + * Loads an array of module directories. All modules will be registered first. Then all will be activated. + */ + function loadModules( required array moduleDirectories ) { + if( !moduleDirectories.len() ) { + return; + } + var moduleService = wirebox.getInstance( 'moduleService' ); + + // Do a pass to convert the array of paths into a struct where the key is the module name and the value is the invocation path + var modulesToLoad = moduleDirectories.reduce( (modulesToLoad,moduleDirectory)=>{ + // Expand path relative to the task CFC. + moduleDirectory = resolvePath( moduleDirectory ); + + // A little validation... + if( !directoryExists( moduleDirectory ) ) { + error( 'Cannot load module. Path [#moduleDirectory#] doesn''t exist.' ); + } + + // Generate a CF mapping that points to the module's folder + var relativeModulePath = fileSystemUtil.makePathRelative( moduleDirectory ); + + // A dot delimited path that points to the folder containing the module + var invocationPath = relativeModulePath + .listChangeDelims( '.', '/\' ) + .listDeleteAt( relativeModulePath.listLen( '/\' ), '.' ); + + // The name of the module + var moduleName = relativeModulePath.listLast( '/\' ); + + modulesToLoad[ moduleName ] = invocationPath; + return modulesToLoad; + } ); + + // Register all of them + modulesToLoad.each( (moduleName,invocationPath)=>moduleService.registerModule( moduleName, invocationPath ) ); + // Activate all of them + modulesToLoad.each( (moduleName)=>moduleService.activateModule( moduleName ) ); + } + /** * This resolves an absolute or relative path using the rules of the operating system and CLI. * It doesn't follow CF mappings and will also always return a trailing slash if pointing to diff --git a/src/cfml/system/Shell.cfc b/src/cfml/system/Shell.cfc index ab40e955..1a5681f5 100644 --- a/src/cfml/system/Shell.cfc +++ b/src/cfml/system/Shell.cfc @@ -28,6 +28,7 @@ component accessors="true" singleton { property name="configService" inject="configService"; property name='systemSettings' inject='SystemSettings'; property name='endpointService' inject='endpointService'; + property name='JSONService' inject='JSONService'; /** * The java jline reader class. @@ -872,10 +873,7 @@ component accessors="true" singleton { // We get to output the results ourselves if( !isNull( result ) && !isSimpleValue( result ) ){ - if( isArray( result ) ){ - return variables.reader.getTerminal().writer().printColumns( result ); - } - result = variables.formatterUtil.formatJson( result ); + result = variables.formatterUtil.formatJson( JSON=result, ANSIColors=JSONService.getANSIColors() ); printString( result ); } else if( !isNull( result ) && len( result ) ) { // If there is an active job, print our output through it diff --git a/src/cfml/system/config/CommandBoxDSL.cfc b/src/cfml/system/config/CommandBoxDSL.cfc index a30cd49e..c7b007aa 100644 --- a/src/cfml/system/config/CommandBoxDSL.cfc +++ b/src/cfml/system/config/CommandBoxDSL.cfc @@ -51,6 +51,7 @@ component implements="wirebox.system.ioc.dsl.IDSLBuilder" accessors=true{ case "ConfigSettings" : { return getInjector().getInstance( 'ConfigService' ).getConfigSettings(); } case "interceptorService" : { return getInjector().getInstance( 'interceptorService' ); } case "moduleService" : { return getInjector().getInstance( 'moduleService' ); } + case "asyncManager" : { return getInjector().getAsyncManager(); } } break; diff --git a/src/cfml/system/config/server.schema.json b/src/cfml/system/config/server.schema.json index 0092af1b..eb6251ac 100644 --- a/src/cfml/system/config/server.schema.json +++ b/src/cfml/system/config/server.schema.json @@ -7,46 +7,59 @@ "type": "object", "properties": { "label": { - "title": "Label", + "title": "Tray Item Label", "description": "Text of menu item", "type": "string" }, "action": { - "title": "Action", - "description": "Action to perform when user clicks this menu item. 'openfilesystem', 'openbrowser', or 'stopserver'", + "title": "Tray Item Action", + "description": "Action to perform when user clicks this menu item", + "type": "string", + "enum": [ + "openfilesystem", + "openbrowser", + "stopserver", + "run", + "runAsync", + "runTerminal" + ] + }, + "command": { + "title": "Tray Item Command", + "description": "A command that is run relative to webroot", "type": "string" }, "url": { - "title": "URL", - "description": "Url to open for 'openbrowser' action", + "title": "Tray Item URL", + "description": "URL to open for 'openbrowser' action", "type": "string" }, "disabled": { - "title": "Disabled", + "title": "Tray Item Disabled", "description": "Turn menu item grey and nothing happens when clicking on it", "type": "boolean", "default": false }, "image": { - "title": "Image", + "title": "Tray Item Image", "description": "Path to PNG image to display on menu item next to the label", "type": "string", "default": "" }, "hotkey": { - "title": "Hotkey", + "title": "Tray Item Hotkey", "description": "Keyboard shortcut to choose this menu item", "type": "string", "default": "" }, "path": { - "title": "Path", + "title": "Tray Item Path", "description": "Filesystem path to open for 'openfilesystem' action", "type": "string", "default": "" }, "items": { - "title": "Items", + "title": "Tray Item Submenu Items", "description": "Nested menu items", "type": "array", "minItems": 0, @@ -63,7 +76,7 @@ "type": "object", "properties": { "name": { - "title": "Name", + "title": "Server Name", "description": "The name of the server", "type": "string", "default": "" @@ -81,13 +94,13 @@ "default": "" }, "startTimeout": { - "title": "Server start timeout", + "title": "Server Start Timeout", "description": "The length of time in seconds to wait for the server to start", "type": "number", "default": 240 }, "stopsocket": { - "title": "Stop Socket", + "title": "Server Stop Socket", "description": "The port the server listens on to receive a stop command", "type": "number", "default": 0 @@ -110,6 +123,22 @@ "type": "boolean", "default": false }, + "profile": { + "title": "Server Profile", + "description": "Profile to assign to a server when you start it to configure the default settings", + "type": "string", + "enum": [ + "development", + "production", + "none" + ] + }, + "dockEnable": { + "title": "Dock Enable", + "description": "", + "type": "boolean", + "default": true + }, "trayEnable": { "title": "Tray Enable", "description": "Control whether the server has an associated icon in the system tray", @@ -132,6 +161,16 @@ }, "default": [] }, + "env": { + "title": "Environment Variables", + "description": "Ad-hoc environment variables", + "type": "object", + "additionalProperties": { + "title": "Environment Variable", + "description": "Ad-hoc environment variable" + }, + "default": {} + }, "jvm": { "title": "JVM", "description": "JVM Options", @@ -158,7 +197,16 @@ "args": { "title": "JVM Arguments", "description": "Ad-hoc JVM args for the server such as -X:name", - "type": "string", + "type": [ + "string", + "array" + ], + "items": { + "title": "JVM Argument", + "description": "Ad-hoc JVM arg for the server such as -X:name", + "type": "string", + "default": "" + }, "default": "" }, "javaHome": { @@ -172,6 +220,12 @@ "description": "A Java installation ID. In its entirety, it has the form _____", "type": "string", "default": "" + }, + "properties": { + "title": "JVM Properties", + "description": "Ad-hoc Java system properties", + "type": "object", + "default": {} } } }, @@ -195,29 +249,40 @@ "directoryBrowsing": { "title": "Directory Browsing", "description": "Enables file listing for directories with no welcome file", - "type": "boolean", - "default": false + "type": "boolean" }, "accessLogEnable": { "title": "Access Log Enable", "description": "Enable web server access log", "type": "boolean", - "default": true + "default": false }, - "GZIPEnable": { - "title": "GZIP Enable", - "description": "Enable GZip compression in HTTP responses", + "gzipEnable": { + "title": "Gzip Enable", + "description": "Enable gzip compression in HTTP responses", "type": "boolean", "default": true }, + "gzipPredicate": { + "title": "Gzip Predicate", + "description": "A custom Undertow Predicate that, when true, will trigger gzip for the request", + "type": "string", + "default": "" + }, "welcomeFiles": { "title": "Welcome Files", "description": "A comma-delimited list of files that you would like CommandBox to look for when a user hits a directory", "type": "string", "default": "" }, + "maxRequests": { + "title": "Web Max Requests", + "description": "", + "type": "string", + "default": "" + }, "aliases": { - "title": "Aliases", + "title": "Web Aliases", "description": "Web aliases for the web server, similar to virtual directories", "type": "object", "patternProperties": { @@ -236,7 +301,7 @@ "type": "object", "properties": { "default": { - "title": "Default", + "title": "Error Page Default", "description": "Path to default error page", "type": "string", "default": "" @@ -258,55 +323,140 @@ "type": "object", "properties": { "enable": { - "title": "Enable", + "title": "HTTP Enable", "description": "Enable HTTP for this serer", "type": "boolean", "default": true }, "port": { - "title": "Port", + "title": "HTTP Port", "description": "HTTP port to use", "type": "number", "default": 0 } } }, + "HTTP2": { + "title": "HTTP2 Settings", + "description": "Configure HTTP2", + "type": "object", + "properties": { + "enable": { + "title": "HTTP2 Enable", + "description": "Enable HTTP2 for this serer", + "type": "boolean", + "default": true + } + } + }, "SSL": { "title": "SSL", "description": "Configure the HTTPS listener on the server", "type": "object", "properties": { "enable": { - "title": "Enable", + "title": "SSL Enable", "description": "Enable HTTPS for this server", "type": "boolean", "default": false }, "port": { - "title": "Port", + "title": "SSL Port", "description": "HTTPS port to use", "type": "number", "default": 1443 }, "certFile": { - "title": "Cert File", + "title": "SSL Cert File", "description": "Path to SSL cert file", "type": "string", "default": "" }, "keyFile": { - "title": "Key File", + "title": "SSL Key File", "description": "Path to SSL key file", "type": "string", "default": "" }, "keyPass": { - "title": "Key Pass", + "title": "SSL Key Pass", "description": "Password for SSL key file", "type": "string", "default": "" + }, + "forceSSLRedirect": { + "title": "Force SSL Redirect", + "description": "Whether to redirect all HTTP traffic over to HTTPS using a 301 status code", + "type": "boolean", + "default": false + }, + "HSTS": { + "title": "HSTS", + "description": "HTTP Strict Transport Security configuration", + "type": "object", + "properties": { + "enable": { + "title": "HSTS Enable", + "description": "Whether to add a Strict-Transport-Security HTTP header", + "type": "boolean", + "default": false + }, + "maxAge": { + "title": "HSTS Max Age", + "description": "How many seconds to remember to use HTTPS", + "type": "number", + "default": 31536000 + }, + "includeSubDomains": { + "title": "HSTS Include Subdomains", + "description": "Whether the HSTS header applies to all subdomains", + "type": "boolean", + "default": false + } + }, + "required": [ "enable" ] + }, + "clientCert": { + "title": "SSL Client Cert", + "description": "", + "type": "object", + "properties": { + "mode": { + "title": "Client Cert Mode", + "description": "", + "type": "string", + "default": "" + }, + "CACertFiles": { + "title": "CA Cert Files", + "description": "", + "type": [ + "string", + "array" + ], + "items": { + "title": "CA Cert File", + "description": "", + "type": "string" + }, + "default": "" + }, + "CATrustStoreFile": { + "title": "CA Trust Store File", + "description": "", + "type": "string", + "default": "" + }, + "CATrustStorePass": { + "title": "CA Trust Store Pass", + "description": "", + "type": "string", + "default": "" + } + } } - } + }, + "required": [ "enable" ] }, "AJP": { "title": "AJP", @@ -314,18 +464,25 @@ "type": "object", "properties": { "enable": { - "title": "Enable", + "title": "AJP Enable", "description": "Enable AJP for this server", "type": "boolean", "default": false }, "port": { - "title": "Port", + "title": "AJP Port", "description": "AJP port to use", "type": "number", "default": 8009 + }, + "secret": { + "title": "AJP Secret", + "description": "An AJP secret to ensure all requests coming into the AJP listener are from a trusted source", + "type": "string", + "default": "" } - } + }, + "required": [ "enable" ] }, "rewrites": { "title": "Rewrites", @@ -333,22 +490,21 @@ "type": "object", "properties": { "enable": { - "title": "Enable", + "title": "Rewrites Enable", "description": "Enable URL Rewrites on this server", "type": "boolean", "default": false }, "logEnable": { - "title": "Log Enable", + "title": "Rewrites Log Enable", "description": "Enable Rewrite log file", "type": "boolean", "default": false }, "config": { - "title": "Config", - "description": "Path to xml config file or .htaccess", - "type": "string", - "default": "" + "title": "Rewrites Config", + "description": "Path to XML config file or .htaccess", + "type": "string" }, "statusPath": { "title": "Tuckey Status Path", @@ -361,21 +517,22 @@ "description": "Number of seconds to check rewrite config file for changes", "type": "number" } - } + }, + "required": [ "enable" ] }, "basicAuth": { - "title": "Configure basic authentication", - "description": "", + "title": "Basic Authentication", + "description": "Configure basic authentication", "type": "object", "properties": { "enable": { - "title": "Enable", + "title": "Basic Auth Enable", "description": "Enable basic auth for this server", "type": "boolean", "default": true }, "users": { - "title": "Users", + "title": "Basic Auth Users", "description": "Users who can authenticate to basic auth", "type": "object", "additionalProperties": { @@ -386,6 +543,157 @@ "default": {} } } + }, + "blockCFAdmin": { + "title": "Block CF Admin", + "description": "", + "type": [ + "boolean", + "string" + ], + "default": "" + }, + "blockSensitivePaths": { + "title": "Block Sensitive Paths", + "description": "", + "type": "boolean" + }, + "blockFlashRemoting": { + "title": "Block Flash Remoting", + "description": "", + "type": "boolean" + }, + "rules": { + "title": "Web Rules", + "description": "Ad-hoc rules using Undertow predicates and handlers", + "type": "array", + "items": { + "title": "Web Rule", + "description": "Ad-hoc rule using Undertow predicates and handlers", + "type": "string" + }, + "default": [] + }, + "rulesFile": { + "title": "Web Rules File", + "description": "A path or paths to files containing Undertow predicates and handlers", + "type": [ + "string", + "array" + ], + "items": { + "title": "Web Rules File", + "description": "A path to file containing Undertow predicates and handlers", + "type": "string" + }, + "default": [] + }, + "allowedExt": { + "title": "Web Allowed Ext", + "description": "A comma-delimited list of additional file extensions allowed by web server", + "type": "string", + "default": "" + }, + "useProxyForwardedIP": { + "title": "Use Proxy Forwarded IP", + "description": "Whether the remote IP in your CF engine's cgi scope represents the upstream IP", + "type": "boolean", + "default": false + }, + "security": { + "title": "Web Security", + "description": "Configure web security", + "type": "object", + "properties": { + "realm": { + "title": "Realm", + "description": "", + "type": "string", + "default": "" + }, + "authPredicate": { + "title": "Auth Predicate", + "description": "", + "type": "string", + "default": "" + }, + "basicAuth": { + "title": "Basic Authentication", + "description": "Configure basic authentication", + "type": "object", + "properties": { + "enable": { + "title": "Basic Auth Enable", + "description": "Enable basic auth for this server", + "type": "boolean" + }, + "users": { + "title": "Basic Auth Users", + "description": "Users who can authenticate to basic auth", + "type": "object", + "additionalProperties": { + "title": "User", + "description": "The key is the user name and the value is the password.", + "type": "string" + } + } + } + }, + "clientCert": { + "title": "Web Security Client Cert", + "description": "", + "type": "object", + "properties": { + "enable": { + "title": "Client Cert Enable", + "description": "", + "type": "boolean", + "default": false + }, + "SSLRenegotiationEnable": { + "title": "Client Cert SSL Renegotiation Enable", + "description": "", + "type": "boolean", + "default": false + }, + "trustUpstreamHeaders": { + "title": "Client Cert Trust Upstream Headers", + "description": "", + "type": "boolean", + "default": false + }, + "subjectDNs": { + "title": "Client Cert Subject DNs", + "description": "", + "type": [ + "string", + "array" + ], + "items": { + "title": "Client Cert Subject DN", + "description": "", + "type": "string" + }, + "default": "" + }, + "issuerDNs": { + "title": "Client Cert Issuer DNs", + "description": "", + "type": [ + "string", + "array" + ], + "items": { + "title": "Client Cert Issuer DN", + "description": "", + "type": "string" + }, + "default": "" + } + }, + "required": [ "enable" ] + } + } } } }, @@ -424,9 +732,21 @@ "type": "string", "default": "" }, + "webXMLOverride": { + "title": "Web XML Override", + "description": "Path to web-override.xml file", + "type": "string", + "default": "" + }, + "webXMLOverrideForce": { + "title": "Web XML Override Force", + "description": "Whether to override any configuration explicitly provided in the override file, as opposed to just adding or updating", + "type": "boolean", + "default": false + }, "WARPath": { "title": "WAR Path", - "description": "Path to a local WAR archive or exploded WAR folder. Mutually exclusive with cfengine.", + "description": "Path to a local WAR archive or exploded WAR folder. Mutually exclusive with cfengine.", "type": "string", "default": "" }, @@ -438,7 +758,7 @@ }, "restMappings": { "title": "REST Mappings", - "description": "Comma delimited list of paths to map to the CF engine's REST servlet such as '/rest/*,/api/*'", + "description": "Comma-delimited list of paths to map to the CF engine's REST servlet such as '/rest/*,/api/*'", "type": "string", "default": "" }, @@ -463,17 +783,131 @@ } }, "runwar": { - "title": "Configure RunWar", - "description": "These settings apply to the underlying Runwar library that starts servers", + "title": "Configure RunWAR", + "description": "These settings apply to the underlying RunWAR library that starts servers", "type": "object", "properties": { + "jarPath": { + "title": "RunWAR JAR Path", + "description": "Path to RunWAR JAR", + "type": "string" + }, "args": { - "title": "Arguments", - "description": "Ad-hoc options for the underlying Runwar library", - "type": "string", + "title": "RunWAR Arguments", + "description": "Ad-hoc options for the underlying RunWAR library", + "type": [ + "string", + "array" + ], + "items": { + "title": "RunWAR Argument", + "description": "Ad-hoc option for the underlying RunWAR library", + "type": "string" + }, "default": "" + }, + "XNIOOptions": { + "title": "XNIO Options", + "description": "Set of options that apply to the low level network transport functions it provides", + "type": "object", + "additionalProperties": { + "title": "XNIO Option", + "description": "Option that applies to the low level network transport functions it provides" + }, + "default": {} + }, + "undertowOptions": { + "title": "Undertow Options", + "description": "Settings that apply to the servlet and web server aspects of Undertow", + "type": "object", + "additionalProperties": { + "title": "Undertow Option", + "description": "Setting that applies to the servlet and web server aspects of Undertow" + }, + "default": {} } } + }, + "ModCFML": { + "title": "ModCFML", + "description": "Configuration around ModCFML standard", + "type": "object", + "properties": { + "enable": { + "title": "ModCFML Enable", + "description": "Whether to enable ModCFML", + "type": "boolean", + "default": false + }, + "maxContexts": { + "title": "ModCFML Max Contexts", + "description": "Limits the number of contexts which can be created", + "type": "number", + "default": 200 + }, + "sharedKey": { + "title": "ModCFML Shared Key", + "description": "Key shared with the web server", + "type": "string", + "default": "" + }, + "requireSharedKey": { + "title": "ModCFML Require Shared Key", + "description": "Whether to require the shared key header to be present", + "type": "boolean", + "default": true + }, + "createVirtualDirectories": { + "title": "ModCFML Create Virtual Directories", + "description": "", + "type": "boolean", + "default": true + } + }, + "required": [ "enable" ] + }, + "scripts": { + "title": "Server Scripts", + "description": "", + "type": "object", + "properties": { + "preServerStart": { + "title": "Pre Server Start Script", + "description": "Runs before any configuration is resolved", + "type": "string" + }, + "onServerStart": { + "title": "On Server Start Script", + "description": "Runs after configuration is resolved but before the actual server starts", + "type": "string" + }, + "onServerInstall": { + "title": "On Server Install Script", + "description": "Runs when engine is being installed during server startup", + "type": "string" + }, + "onServerStop": { + "title": "On Server Stop Script", + "description": "Runs before a server stop", + "type": "string" + }, + "preServerForget": { + "title": "Pre Server Forget Script", + "description": "Runs before attempting to forget a server", + "type": "string" + }, + "postServerForget": { + "title": "Post Server Forget Script", + "description": "Runs after a successful server forget", + "type": "string" + } + }, + "additionalProperties": { + "title": "Server Script", + "description": "Ad-hoc server script", + "type": "string" + }, + "default": {} } } -} \ No newline at end of file +} diff --git a/src/cfml/system/endpoints/ForgeBox.cfc b/src/cfml/system/endpoints/ForgeBox.cfc index acb6ed8d..8ddb3c10 100644 --- a/src/cfml/system/endpoints/ForgeBox.cfc +++ b/src/cfml/system/endpoints/ForgeBox.cfc @@ -276,7 +276,7 @@ component accessors="true" implements="IEndpointInteractive" { // Check for no ext or .txt or .md in reverse precedence. for( var ext in [ '', '.txt', '.md' ] ) { // Case insensitive search for file name - var files = directoryList( path=arguments.path, filter=function( path ){ return path contains ( item.file & ext); } );0 + var files = directoryList( path=arguments.path, filter=function( path ){ return path contains ( item.file & ext); } ); if( arrayLen( files ) && fileExists( files[ 1 ] ) ) { // If found, read in the first one found. props[ item.variable ] = fileRead( files[ 1 ], 'UTF-8' ); @@ -445,7 +445,7 @@ component accessors="true" implements="IEndpointInteractive" { * @package The full endpointID like foo@1.0.0 */ public function parseSlug( required string package ) { - var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); + var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)(?!x\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); if ( arrayLen( matches.len ) < 2 ) { throw( type = "endpointException", @@ -462,7 +462,7 @@ component accessors="true" implements="IEndpointInteractive" { public function parseVersion( required string package ) { var version = 'stable'; // foo@1.0.0 - var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); + var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)(?!x\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); if ( matches.pos.len() >= 3 && matches.pos[ 3 ] != 0 ) { // Note this can also be a semver range like 1.2.x, >2.0.0, or 1.0.4-2.x // For now I'm assuming it's a specific version diff --git a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/model-test.cfc b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/model-test.cfc index 9d5174be..5d75857f 100644 --- a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/model-test.cfc +++ b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/create/model-test.cfc @@ -53,7 +53,13 @@ component { arguments.path, "all" ); - + modelTestContent = replaceNoCase( + modelTestContent, + "|modelPath|", + arguments.path, + "all" + ); + // Handle Methods if ( len( arguments.methods ) ) { var allTestsCases = ""; diff --git a/src/cfml/system/modules_app/server-commands/commands/server/restart.cfc b/src/cfml/system/modules_app/server-commands/commands/server/restart.cfc index 42b96356..99724f48 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/restart.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/restart.cfc @@ -1,5 +1,5 @@ /** - * Resstart an embedded CFML server. Run command from the web root of the server or use + * Restart an embedded CFML server. Run command from the web root of the server or use * the 'directory' and/or 'name' arguments. * . * {code:bash} diff --git a/src/cfml/system/modules_app/task-commands/models/TaskService.cfc b/src/cfml/system/modules_app/task-commands/models/TaskService.cfc index 8a6059ad..042c457f 100644 --- a/src/cfml/system/modules_app/task-commands/models/TaskService.cfc +++ b/src/cfml/system/modules_app/task-commands/models/TaskService.cfc @@ -231,23 +231,36 @@ component singleton accessors=true { // Create this Task CFC try { + var taskCaching = ConfigService.getSetting( 'taskCaching', false ); var mappingName = "task-" & relTaskFile; - if( !ConfigService.getSetting( 'taskCaching', false ) && wirebox.getBinder().mappingExists( mappingName ) ) { - // Clear it so metadata can be refreshed. - wirebox.getBinder().unMap( mappingName ); - } + // If we're not caching tasks, single thread this whole block so we don't get mapping undefined errors + // if running the same task concurrently. + lock name='create-#mappingName#' type='#taskCaching ? 'readonly' : 'exclusive'#' timeout=20 { - // Check if task mapped? - if( !wirebox.getBinder().mappingExists( mappingName ) ){ - // feed this task to wirebox with virtual inheritance - wirebox.registerNewInstance( name=mappingName, instancePath=relTaskFile ) - .setVirtualInheritance( "commandbox.system.BaseTask" ); - } + if( !taskCaching && wirebox.getBinder().mappingExists( mappingName ) ) { + // Clear it so metadata can be refreshed. + wirebox.getBinder().unMap( mappingName ); + } - // retrieve, build and wire from wirebox - return wireBox.getInstance( mappingName ); + // Check if task mapped? + if( !wirebox.getBinder().mappingExists( mappingName ) ){ + // Double check lock to prevent two threads from both creating the mapping + // This lock will be effectivley moot if task caching is disabled since we'll already be in an + // exclusive lock, but will be neccessary if task caching is on + lock name='map-#mappingName#' type='exclusive' timeout=20 { + if( !wirebox.getBinder().mappingExists( mappingName ) ){ + // feed this task to wirebox with virtual inheritance + wirebox.registerNewInstance( name=mappingName, instancePath=relTaskFile ) + .setVirtualInheritance( "commandbox.system.BaseTask" ); + } + } + } + + // retrieve, build and wire from wirebox + return wireBox.getInstance( mappingName ); + } // This will catch nasty parse errors and tell us where they happened } catch( any e ){ // Log the full exception with stack trace diff --git a/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc b/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc index 986a56ae..090cfe1f 100644 --- a/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc +++ b/src/cfml/system/modules_app/testbox-commands/commands/testbox/run.cfc @@ -154,6 +154,7 @@ component { } // User our Renderer to publish the nice results + var boxOptions = packageService.readPackageDescriptor( getCWD() ).testbox; CLIRenderer.render( print, testData, diff --git a/src/cfml/system/services/EndpointService.cfc b/src/cfml/system/services/EndpointService.cfc index 1a9b2a50..2fd847cb 100644 --- a/src/cfml/system/services/EndpointService.cfc +++ b/src/cfml/system/services/EndpointService.cfc @@ -152,11 +152,23 @@ component accessors="true" singleton { // Endpoint is specified as "endpoint:resource" } else if( listLen( arguments.ID, ':' ) > 1 ) { var endpointName = listFirst( arguments.ID, ':' ); + var package = listRest( arguments.ID, ':' ); if( structKeyExists( getEndpointRegistry(), endpointName ) ) { + var theID = arguments.ID; + if( endpointName == 'file' || endpointName == 'folder' ) { + package = fileSystemUtil.resolvePath( package, arguments.currentWorkingDirectory ); + if( endpointName == 'file' && !fileExists( package ) ) { + throw( "The file [ #package# ] does not exist.", 'endpointException' ); + } + if( endpointName == 'folder' && !directoryExists( package ) ) { + throw( "The folder [ #package# ] does not exist.", 'endpointException' ); + } + theID = endpointName & ':' & package; + } return { endpointName : endpointName, - package : listRest( arguments.ID, ':' ), - ID : arguments.ID + package : package, + ID : theID }; } else { if( listFindNoCase( 'C,D,E,F,G,H', endpointName ) ) { diff --git a/src/cfml/system/services/JSONService.cfc b/src/cfml/system/services/JSONService.cfc index b6fe8e3d..2e60fc78 100644 --- a/src/cfml/system/services/JSONService.cfc +++ b/src/cfml/system/services/JSONService.cfc @@ -168,7 +168,7 @@ component accessors="true" singleton { var fullPropertyName = 'arguments.JSON' & toBracketNotation( arguments.property ); if( !isDefined( fullPropertyName ) ) { - + throw( message='#arguments.property# does not exist.', type="JSONException"); } // Get the array reference @@ -187,7 +187,7 @@ component accessors="true" singleton { last = last.right(-1).left(-1); last = parser.unwrapQuotes( trim( last ) ) } - + // path to containing struct var everythingBut = propArray.slice( 1, propArray.len()-1 ); @@ -237,9 +237,9 @@ component accessors="true" singleton { } return fullPropertyName; } - + function tokenizeProp( required string str ) { - + // Holds token var tokens = []; // Used to build up each token @@ -277,14 +277,14 @@ component accessors="true" singleton { if( inBrackets ) { token &= char; - + if( char == ']' ) { inBrackets = false; } prevChar = char; continue; } - + // period or break in brackets means break in token if( ( char == '.' && !inBrackets ) || char == '[' ) { @@ -292,7 +292,7 @@ component accessors="true" singleton { if( ( char == '[' ) ) { inBrackets = true; } - + if( len( token ) ) { tokens.append( token ); token = ''; @@ -323,7 +323,7 @@ component accessors="true" singleton { if( len( token ) ) { tokens.append( token ); } - + return tokens; } @@ -364,7 +364,15 @@ component accessors="true" singleton { // Recursive function to crawl struct and create a string that represents each property. function addProp( props, prop, safeProp, targetStruct ) { - var propValue = ( len( prop ) ? evaluate( 'targetStruct#safeProp#' ) : targetStruct ); + if( len( prop ) ) { + // Handle null key + if( !isDefined( 'targetStruct#safeProp#' ) ) { + return props; + } + var propValue = evaluate( 'targetStruct#safeProp#' ) + } else { + var propValue = targetStruct; + } if( isStruct( propValue ) ) { // Add all of this struct's keys diff --git a/src/cfml/system/services/PackageService.cfc b/src/cfml/system/services/PackageService.cfc index d333071e..d242bf47 100644 --- a/src/cfml/system/services/PackageService.cfc +++ b/src/cfml/system/services/PackageService.cfc @@ -590,8 +590,18 @@ component accessors="true" singleton { var isSaving = ( arguments.save || arguments.saveDev ); var detail = dependencies[ dependency ]; + var endpointName = 'forgebox'; + try { + var endpointData = endpointService.resolveEndpointData( detail, installDirectory ); + endpointName = endpointData.endpointName; + } catch ( EndpointNotFound e ) { + // Ignore + } catch( any e ) { + rethrow; + } + // full ID with endpoint and package like file:/opt/files/foo.zip - if( detail contains ':' ) { + if( endpointName != 'forgebox' ) { var ID = detail; // Default ForgeBox endpoint of foo@1.0.0 } else { @@ -1348,7 +1358,7 @@ component accessors="true" singleton { * @package The full endpointID like foo@1.0.0 */ private function parseSlug( required string package ) { - var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); + var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)(?!x\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); if ( arrayLen( matches.len ) < 2 ) { throw( type = "endpointException", @@ -1365,7 +1375,7 @@ component accessors="true" singleton { private function parseVersion( required string package ) { var version = ''; // foo@1.0.0 - var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); + var matches = REFindNoCase( "^([\w\-\.]+(?:\@(?!stable\b)(?!be\b)(?!x\b)[a-zA-Z][\w\-]*)?)(?:\@(.+))?$", package, 1, true ); if ( matches.pos.len() >= 3 && matches.pos[ 3 ] != 0 ) { // Note this can also be a semver range like 1.2.x, >2.0.0, or 1.0.4-2.x // For now I'm assuming it's a specific version diff --git a/src/cfml/system/services/ServerEngineService.cfc b/src/cfml/system/services/ServerEngineService.cfc index a80f4327..09d556cd 100644 --- a/src/cfml/system/services/ServerEngineService.cfc +++ b/src/cfml/system/services/ServerEngineService.cfc @@ -144,11 +144,19 @@ component accessors="true" singleton="true" { installDir : '', initialInstall : false }; + + // If CFEngine is a relateive file path, we need to know where to look for it. + var currentWorkingDirectory = serverInfo.webroot; + if( serverInfo.cfengineSource == 'serverJSON' ) { + currentWorkingDirectory = getDirectoryFromPath( serverInfo.serverConfigFile ); + } else if( serverInfo.cfengineSource == 'serverProps' ) { + currentWorkingDirectory = shell.pwd(); + } var thisTempDir = tempDir & '/' & createUUID(); // Find out what endpoint will service them and ask the endpoint what their name is. - var endpointData = endpointService.resolveEndpoint( ID, shell.pwd() ); + var endpointData = endpointService.resolveEndpoint( ID, currentWorkingDirectory ); var endpoint = endpointData.endpoint; var engineName = endpoint.getDefaultName( arguments.ID ); installDetails.engineName = engineName; @@ -302,7 +310,12 @@ component accessors="true" singleton="true" { return installDetails; } - if( !packageService.installPackage( ID=arguments.ID, directory=thisTempDir, save=false ) ) { + if( !packageService.installPackage( + ID=arguments.ID, + directory=thisTempDir, + save=false, + currentWorkingDirectory=currentWorkingDirectory ) + ) { throw( message='Server not installed.', type="commandException"); } diff --git a/src/cfml/system/services/ServerService.cfc b/src/cfml/system/services/ServerService.cfc index 856bb182..217c46e6 100644 --- a/src/cfml/system/services/ServerService.cfc +++ b/src/cfml/system/services/ServerService.cfc @@ -173,7 +173,13 @@ component accessors="true" singleton { 'HSTS' : { 'enable' : d.web.ssl.hsts.enable ?: false, 'maxAge' : d.web.ssl.hsts.maxAge ?: 31536000, - 'includeSubDomains' : d.web.ssl.hsts.includeSubDomains ?: false + 'includeSubDomains' : d.web.ssl.hsts.includeSubDomains ?: false + }, + 'clientCert' : { + 'mode' : d.web.ssl.clientCert.mode ?: '', + 'CACertFiles' : d.web.ssl.clientCert.CACertFiles ?: '', + 'CATrustStoreFile' : d.web.ssl.clientCert.CATrustStoreFile ?: '', + 'CATrustStorePass' : d.web.ssl.clientCert.CATrustStorePass ?: '' } }, 'AJP' : { @@ -203,7 +209,22 @@ component accessors="true" singleton { 'blockSensitivePaths' : d.web.blockSensitivePaths ?: '', 'blockFlashRemoting' : d.web.blockFlashRemoting ?: '', 'allowedExt' : d.web.allowedExt ?: '', - 'useProxyForwardedIP' : d.web.useProxyForwardedIP ?: false + 'useProxyForwardedIP' : d.web.useProxyForwardedIP ?: false, + 'security' : { + 'realm' : d.web.security.realm ?: '', + 'authPredicate' : d.web.security.authPredicate ?: '', + 'basicAuth' : { + 'enable' : d.web.security.basicAuth.enable ?: nullvalue(), + 'users' : d.web.security.basicAuth.users ?: nullvalue() + }, + 'clientCert' : { + 'enable' : d.web.security.clientCert.enable ?: false, + 'SSLRenegotiationEnable' : d.web.security.clientCert.SSLRenegotiationEnable ?: false, + 'trustUpstreamHeaders' : d.web.security.clientCert.trustUpstreamHeaders ?: false, + 'subjectDNs' : d.web.security.clientCert.subjectDNs ?: '', + 'issuerDNs' : d.web.security.clientCert.issuerDNs ?: '' + } + } }, 'app' : { 'logDir' : d.app.logDir ?: '', @@ -319,7 +340,7 @@ component accessors="true" singleton { systemSettings.expandDeepSystemSettings( serverJSON ); systemSettings.expandDeepSystemSettings( defaults ); - + // Mix in environment variable overrides like BOX_SERVER_PROFILE loadOverrides( serverJSON, serverInfo, serverProps.verbose ?: serverJSON.verbose ?: defaults.verbose ?: false ); @@ -757,6 +778,37 @@ component accessors="true" singleton { if( len( defaults.web.SSL.keyFile ?: '' ) ) { defaults.web.SSL.keyFile = fileSystemUtil.resolvePath( defaults.web.SSL.keyFile, defaultwebroot ); } serverInfo.SSLKeyFile = serverProps.SSLKeyFile ?: serverJSON.web.SSL.keyFile ?: defaults.web.SSL.keyFile; + // relative certFile in server.json is resolved relative to the server.json + if( isDefined( 'serverJSON.web.SSL.clientCert.CACertFiles' ) ) { + if( isSimpleValue( serverJSON.web.SSL.clientCert.CACertFiles ) ) { + serverJSON.web.SSL.clientCert.CACertFiles = listToArray( serverJSON.web.SSL.clientCert.CACertFiles ); + } + serverJSON.web.SSL.clientCert.CACertFiles = serverJSON.web.SSL.clientCert.CACertFiles.map( (f)=>fileSystemUtil.resolvePath( f, defaultServerConfigFileDirectory ) ); + } + // relative certFile in config setting server defaults is resolved relative to the web root + if( len( defaults.web.SSL.clientCert.CACertFiles ) ) { + if( isSimpleValue( defaults.web.SSL.clientCert.CACertFiles ) ) { + defaults.web.SSL.clientCert.CACertFiles = listToArray( defaults.web.SSL.clientCert.CACertFiles ); + } + defaults.web.SSL.clientCert.CACertFiles = defaults.web.SSL.clientCert.CACertFiles.map( (f)=>fileSystemUtil.resolvePath( f, defaultwebroot ) ); + } else { + defaults.web.SSL.clientCert.CACertFiles = []; + } + serverInfo.clientCertCACertFiles = serverJSON.web.SSL.clientCert.CACertFiles ?: defaults.web.SSL.clientCert.CACertFiles; + + if( !isNull( serverJSON.web.SSL.clientCert.CATrustStoreFile ) ) { + serverJSON.web.SSL.clientCert.CATrustStoreFile = fileSystemUtil.resolvePath( serverJSON.web.SSL.clientCert.CATrustStoreFile, defaultServerConfigFileDirectory ); + } + if( len( defaults.web.SSL.clientCert.CATrustStoreFile ) ) { + defaults.web.SSL.clientCert.CATrustStoreFile = fileSystemUtil.resolvePath( defaults.web.SSL.clientCert.CATrustStoreFile, defaultwebroot ); + } + serverInfo.clientCertCATrustStoreFile = serverJSON.web.SSL.clientCert.CATrustStoreFile ?: defaults.web.SSL.clientCert.CATrustStoreFile; + serverInfo.clientCertCATrustStorePass = serverJSON.web.SSL.clientCert.CATrustStorePass ?: defaults.web.SSL.clientCert.CATrustStorePass; + + serverInfo.clientCertMode = serverJSON.web.SSL.clientCert.mode ?: defaults.web.SSL.clientCert.mode; + serverInfo.clientCertSSLRenegotiationEnable = serverJSON.web.security.clientCert.SSLRenegotiationEnable ?: defaults.web.security.clientCert.SSLRenegotiationEnable; + + serverInfo.SSLForceRedirect = serverJSON.web.SSL.forceSSLRedirect ?: defaults.web.SSL.forceSSLRedirect; serverInfo.HSTSEnable = serverJSON.web.SSL.HSTS.enable ?: defaults.web.SSL.HSTS.enable; serverInfo.HSTSMaxAge = serverJSON.web.SSL.HSTS.maxAge ?: defaults.web.SSL.HSTS.maxAge; @@ -766,8 +818,59 @@ component accessors="true" singleton { serverInfo.rewritesEnable = serverProps.rewritesEnable ?: serverJSON.web.rewrites.enable ?: defaults.web.rewrites.enable; serverInfo.rewritesStatusPath = serverJSON.web.rewrites.statusPath ?: defaults.web.rewrites.statusPath; serverInfo.rewritesConfigReloadSeconds = serverJSON.web.rewrites.configReloadSeconds ?: defaults.web.rewrites.configReloadSeconds; - serverInfo.basicAuthEnable = serverJSON.web.basicAuth.enable ?: defaults.web.basicAuth.enable; - serverInfo.basicAuthUsers = serverJSON.web.basicAuth.users ?: defaults.web.basicAuth.users; + + serverInfo.basicAuthEnable = serverJSON.web.security.basicAuth.enable ?: defaults.web.security.basicAuth.enable ?: serverJSON.web.basicAuth.enable ?: defaults.web.basicAuth.enable; + serverInfo.basicAuthUsers = serverJSON.web.security.basicAuth.users ?: defaults.web.security.basicAuth.users ?: serverJSON.web.basicAuth.users ?: defaults.web.basicAuth.users; + // If there are no users, basic auth is NOT enabled + if( !serverInfo.basicAuthUsers.count() ) { + serverInfo.basicAuthEnable = false; + } + + serverInfo.clientCertEnable = serverJSON.web.security.clientCert.enable ?: defaults.web.security.clientCert.enable; + serverInfo.clientCertTrustUpstreamHeaders = serverJSON.web.security.clientCert.trustUpstreamHeaders ?: defaults.web.security.clientCert.trustUpstreamHeaders; + + // Default missing values + serverJSON.web.security.clientCert.subjectDNs = serverJSON.web.security.clientCert.subjectDNs ?: ''; + serverJSON.web.security.clientCert.issuerDNs = serverJSON.web.security.clientCert.issuerDNs ?: ''; + + // Convert all strings to arrays + if( isSimpleValue( serverJSON.web.security.clientCert.subjectDNs ) ) { + if( len( serverJSON.web.security.clientCert.subjectDNs ) ) { + serverJSON.web.security.clientCert.subjectDNs = [ serverJSON.web.security.clientCert.subjectDNs ]; + } else { + serverJSON.web.security.clientCert.subjectDNs = []; + } + } + if( isSimpleValue( defaults.web.security.clientCert.subjectDNs ) ) { + if( len( defaults.web.security.clientCert.subjectDNs ) ) { + defaults.web.security.clientCert.subjectDNs = [ defaults.web.security.clientCert.subjectDNs ]; + } else { + defaults.web.security.clientCert.subjectDNs = []; + } + } + if( isSimpleValue( serverJSON.web.security.clientCert.issuerDNs ) ) { + if( len( serverJSON.web.security.clientCert.issuerDNs ) ) { + serverJSON.web.security.clientCert.issuerDNs = [ serverJSON.web.security.clientCert.issuerDNs ]; + } else { + serverJSON.web.security.clientCert.issuerDNs = []; + } + } + if( isSimpleValue( defaults.web.security.clientCert.issuerDNs ) ) { + if( len( defaults.web.security.clientCert.issuerDNs ) ) { + defaults.web.security.clientCert.issuerDNs = [ defaults.web.security.clientCert.issuerDNs ]; + } else { + defaults.web.security.clientCert.issuerDNs = []; + } + } + + // Combine server defaults AND any settings in server.json + serverInfo.clientCertSubjectDNs = serverJSON.web.security.clientCert.subjectDNs.append( defaults.web.security.clientCert.subjectDNs, true ); + serverInfo.clientCertIssuerDNs = serverJSON.web.security.clientCert.issuerDNs.append( defaults.web.security.clientCert.issuerDNs, true ); + + serverInfo.authEnabled = serverInfo.basicAuthEnable || serverInfo.clientCertEnable; + serverInfo.securityRealm = serverJSON.web.security.realm ?: defaults.web.security.realm; + serverInfo.authPredicate = serverJSON.web.security.authPredicate ?: defaults.web.security.authPredicate; + serverInfo.welcomeFiles = serverProps.welcomeFiles ?: serverJSON.web.welcomeFiles ?: defaults.web.welcomeFiles; serverInfo.maxRequests = serverJSON.web.maxRequests ?: defaults.web.maxRequests; @@ -1037,6 +1140,13 @@ component accessors="true" singleton { } serverInfo.cfengine = serverProps.cfengine ?: serverJSON.app.cfengine ?: defaults.app.cfengine; + serverInfo.cfengineSource = 'defaults'; + if( !isNull( serverJSON.app.cfengine ) ) { + serverInfo.cfengineSource = 'serverJSON'; + } + if( !isNull( serverProps.cfengine ) ) { + serverInfo.cfengineSource = 'serverProps'; + } serverInfo.restMappings = serverProps.restMappings ?: serverJSON.app.restMappings ?: defaults.app.restMappings; // relative rewrite config path in server.json is resolved relative to the server.json @@ -1559,14 +1669,30 @@ component accessors="true" singleton { } // Send SSL cert info if SSL is enabled and there's cert info - if( serverInfo.SSLEnable && serverInfo.SSLCertFile.len() ) { - args - .append( '--ssl-cert' ).append( serverInfo.SSLCertFile ) - .append( '--ssl-key' ).append( serverInfo.SSLKeyFile ); - // Not all certs require a password - if( serverInfo.SSLKeyPass.len() ) { - args.append( '--ssl-keypass' ).append( serverInfo.SSLKeyPass ); + if( serverInfo.SSLEnable ) { + if( serverInfo.SSLCertFile.len() ) { + args + .append( '--ssl-cert' ).append( serverInfo.SSLCertFile ) + .append( '--ssl-key' ).append( serverInfo.SSLKeyFile ); + // Not all certs require a password + if( serverInfo.SSLKeyPass.len() ) { + args.append( '--ssl-keypass' ).append( serverInfo.SSLKeyPass ); + } + } + if( len( serverInfo.clientCertMode ) ){ + args.append( '--client-cert-negotiation' ).append( serverInfo.clientCertMode ); + } + if( serverInfo.clientCertSSLRenegotiationEnable ) { + args.append( '--client-cert-renegotiation' ).append( serverInfo.clientCertSSLRenegotiationEnable ); + } + if( len( serverInfo.clientCertCATrustStoreFile ) ) { + args.append( '--ssl-add-ca-truststore' ).append( serverInfo.clientCertCATrustStoreFile ); + args.append( '--ssl-add-ca-truststore-pass' ).append( serverInfo.clientCertCATrustStorePass ); } + if( serverInfo.clientCertCACertFiles.len() ){ + args.append( '--ssl-add-ca-certs' ).append( serverInfo.clientCertCACertFiles.toList() ); + } + } // Incorporate rewrites to command @@ -1579,18 +1705,40 @@ component accessors="true" singleton { args.append( '--urlrewrite-check' ).append( serverInfo.rewritesConfigReloadSeconds ); } - // Basic auth - if( serverInfo.basicAuthEnable && serverInfo.basicAuthUsers.count() ) { - // Escape commas and equals with backslash - var sanitizeBA = function( i ) { return i.replace( ',', '\,', 'all' ).replace( '=', '\=', 'all' ); }; - var thisBasicAuthUsers = ''; - serverInfo.basicAuthUsers.each( function( i ) { - thisBasicAuthUsers = thisBasicAuthUsers.listAppend( '#sanitizeBA( i )#=#sanitizeBA( serverInfo.basicAuthUsers[ i ] )#' ); - } ); - // user=pass,user2=pass2 - args.append( '--basicauth-users' ).append( thisBasicAuthUsers ); - } + if( serverInfo.authEnabled ) { + if( len( serverInfo.authPredicate ) ) { + args.append( '--auth-predicate' ).append( serverInfo.authPredicate ); + } + if( !len( serverInfo.securityRealm ) ) { + serverInfo.securityRealm = serverInfo.name; + } + args.append( '--security-realm' ).append( serverInfo.securityRealm ); + + // Basic auth + if( serverInfo.basicAuthEnable ) { + // Escape commas and equals with backslash + var sanitizeBA = function( i ) { return i.replace( ',', '\,', 'all' ).replace( '=', '\=', 'all' ); }; + var thisBasicAuthUsers = ''; + serverInfo.basicAuthUsers.each( function( i ) { + thisBasicAuthUsers = thisBasicAuthUsers.listAppend( '#sanitizeBA( i )#=#sanitizeBA( serverInfo.basicAuthUsers[ i ] )#' ); + } ); + // user=pass,user2=pass2 + args.append( '--basicauth-users' ).append( thisBasicAuthUsers ); + + } + + // Client cert + if( serverInfo.clientCertEnable ) { + args + .append( '--client-cert-enable' ).append( serverInfo.clientCertEnable ) + .append( '--client-cert-subjectdns' ).append( serializeJSON( serverInfo.clientCertSubjectDNs ) ) + .append( '--client-cert-issuerdns' ).append( serializeJSON( serverInfo.clientCertIssuerDNs ) ); + } + } + + args.append( '--client-cert-trust-headers' ).append( serverInfo.clientCertTrustUpstreamHeaders ) + if( serverInfo.rewritesEnable ){ if( !fileExists(serverInfo.rewritesConfig) ){ job.error( 'URL rewrite config not found [#serverInfo.rewritesConfig#]' ); @@ -1670,6 +1818,11 @@ component accessors="true" singleton { job.complete( serverInfo.verbose ); return; } + + + if( fileSystemUtil.isWindows() ) { + args = args.map( (a)=>replace( a, '"', '\"', 'all' ) ); + } processBuilder.init( args ); @@ -1847,9 +2000,9 @@ component accessors="true" singleton { } else { logger.error( '#e.message# #e.detail#' , e.stackTrace ); consoleLogger.error( '#e.message##chr(10)##e.detail#' ); - } + } } - + // Now it's time to shut-er down variables.waitingOnConsoleStart = false; shell.setPrompt(); @@ -2088,6 +2241,9 @@ component accessors="true" singleton { // Get the web root out of the server.json, if specified and make it relative to the actual server.json file. } else if( len( serverJSON.web.webroot ?: '' ) ) { var defaultwebroot = fileSystemUtil.resolvePath( serverJSON.web.webroot, getDirectoryFromPath( defaultServerConfigFile ) ); + // If we found a server.json by conventin and pull the web root from there, let's lock this in so we use it. + // Otherwise, a server.json pointing to another webroot will cause us to try and put the server.json in the external web root + serverProps.serverConfigFile = defaultServerConfigFile; if( locVerbose ) { consoleLogger.debug("webroot pulled from server's JSON: #defaultwebroot#"); } // Otherwise default to the directory the server's JSON file lives in (which defaults to the CWD) } else { @@ -2385,14 +2541,15 @@ component accessors="true" singleton { **/ function isProcessAlive( required pidStr, throwOnError=false ) { var result = ""; - var timeStart = millisecond(now()); try{ if (fileSystemUtil.isWindows() ) { cfexecute(name='cmd', arguments='/c tasklist /FI "PID eq #pidStr#"', variable="result" timeout="10"); + if (findNoCase("java", result) > 0 && findNoCase(pidStr, result) > 0) return true; } else if (fileSystemUtil.isMac() || fileSystemUtil.isLinux() ) { - cfexecute(name='ps', arguments='-p #pidStr#', variable="result" , timeout="10"); + cfexecute(name='ps', arguments='-A -o pid,comm', variable="result" , timeout="10"); + var matchedProcesses = reMatchNoCase("(?m)^\s*#pidStr#\s.*java",result); + if (matchedProcesses.len()) return true; } - if (findNoCase("java", result) > 0 && findNoCase(pidStr, result) > 0) return true; } catch ( any e ){ if( throwOnError ) { rethrow; @@ -2405,12 +2562,26 @@ component accessors="true" singleton { /** * Logic to tell if a server is running * @serverInfo.hint Struct of server information + * @quick When set to true, only the PID file is checked for on disk. When set to false, the OS is actually asked if the process is still running. **/ - function isServerRunning( required struct serverInfo ){ + function isServerRunning( required struct serverInfo, boolean quick=false ){ if(fileExists(serverInfo.pidFile)){ var serverPID = fileRead(serverInfo.pidFile); - thread action="run" name="check_#serverPID##getTickCount()#" serverPID=serverPID pidFile=serverInfo.pidFile { - if(!isProcessAlive(attributes.serverPID,true)) fileDelete(attributes.pidFile) + if( arguments.quick ) { + thread action="run" name="check_#serverPID##getTickCount()#" serverPID=serverPID pidFile=serverInfo.pidFile { + if(!isProcessAlive(attributes.serverPID,true)) { + fileDelete(attributes.pidFile); + } + } + } else { + if(!isProcessAlive(serverPID,true)) { + try { + fileDelete(serverInfo.pidFile); + } catch( any e ) { + // If the file didn't exist, ignore it. + } + return false; + } } return true; } @@ -2684,88 +2855,100 @@ component accessors="true" singleton { 'arguments' : "", 'command' : "" }, - 'name' : "", - 'logDir' : "", - 'consolelogPath' : "", - 'accessLogPath' : "", - 'rewritesLogPath' : "", - 'trayicon' : "", - 'libDirs' : "", - 'webConfigDir' : "", - 'serverConfigDir' : "", - 'serverHomeDirectory' : "", - 'singleServerHome' : false, - 'serverHome' : "", - 'webroot' : "", - 'webXML' : "", - 'webXMLOverride' : "", - 'webXMLOverrideActual' : "", - 'webXMLOverrideForce' : false, - 'HTTPEnable' : true, - 'HTTP2Enable' : true, - 'SSLEnable' : false, - 'SSLPort' : 1443, - 'AJPEnable' : false, - 'AJPPort' : 8009, - 'SSLCertFile' : "", - 'SSLKeyFile' : "", - 'SSLKeyPass' : "", - 'rewritesEnable' : false, - 'rewritesConfig' : "", - 'rewritesStatusPath': "", - 'rewritesConfigReloadSeconds' : "", - 'basicAuthEnable' : true, - 'basicAuthUsers' : {}, - 'heapSize' : '', - 'minHeapSize' : '', - 'javaHome' : '', - 'javaVersion' : '', - 'directoryBrowsing' : false, - 'JVMargs' : "", - 'JVMargsArray' : [], - 'runwarArgs' : "", - 'runwarArgsArray' : [], - 'runwarXNIOOptions' : {}, + 'name' : "", + 'logDir' : "", + 'consolelogPath' : "", + 'accessLogPath' : "", + 'rewritesLogPath' : "", + 'trayicon' : "", + 'libDirs' : "", + 'webConfigDir' : "", + 'serverConfigDir' : "", + 'serverHomeDirectory' : "", + 'singleServerHome' : false, + 'serverHome' : "", + 'webroot' : "", + 'webXML' : "", + 'webXMLOverride' : "", + 'webXMLOverrideActual' : "", + 'webXMLOverrideForce' : false, + 'HTTPEnable' : true, + 'HTTP2Enable' : true, + 'SSLEnable' : false, + 'SSLPort' : 1443, + 'AJPEnable' : false, + 'AJPPort' : 8009, + 'SSLCertFile' : "", + 'SSLKeyFile' : "", + 'SSLKeyPass' : "", + 'clientCertCACertFiles' : [], + 'clientCertMode' : '', + 'clientCertSSLRenegotiationEnable': false, + 'clientCertEnable' : false, + 'clientCertTrustUpstreamHeaders': false, + 'clientCertSubjectDNs' : [], + 'clientCertIssuerDNs' : [], + 'securityRealm' : '', + 'clientCertCATrustStoreFile': '', + 'clientCertCATrustStorePass': '', + 'rewritesEnable' : false, + 'rewritesConfig' : "", + 'rewritesStatusPath' : "", + 'rewritesConfigReloadSeconds': "", + 'basicAuthEnable' : true, + 'authPredicate' : '', + 'basicAuthUsers' : {}, + 'heapSize' : '', + 'minHeapSize' : '', + 'javaHome' : '', + 'javaVersion' : '', + 'directoryBrowsing' : false, + 'JVMargs' : "", + 'JVMargsArray' : [], + 'runwarArgs' : "", + 'runwarArgsArray' : [], + 'runwarXNIOOptions' : {}, 'runwarUndertowOptions' : {}, - 'cfengine' : "", - 'restMappings' : "", + 'cfengine' : "", + 'cfengineSource' : 'defaults', + 'restMappings' : "", 'sessionCookieSecure' : false, 'sessionCookieHTTPOnly' : false, - 'engineName' : "", - 'engineVersion' : "", - 'WARPath' : "", - 'serverConfigFile' : "", - 'aliases' : {}, - 'errorPages' : {}, - 'accessLogEnable' : false, - 'GZipEnable' : true, - 'GZipPredicate' : '', - 'rewritesLogEnable' : false, - 'trayOptions' : {}, - 'trayEnable' : true, - 'dockEnable' : true, - 'dateLastStarted' : '', - 'openBrowser' : true, - 'openBrowserURL' : '', - 'profile' : '', - 'customServerFolder': '', - 'welcomeFiles' : '', - 'maxRequests' : '', - 'exitCode' : 0, - 'rules' : [], - 'rulesFile' : '', - 'blockCFAdmin' : false, + 'engineName' : "", + 'engineVersion' : "", + 'WARPath' : "", + 'serverConfigFile' : "", + 'aliases' : {}, + 'errorPages' : {}, + 'accessLogEnable' : false, + 'GZipEnable' : true, + 'GZipPredicate' : '', + 'rewritesLogEnable' : false, + 'trayOptions' : {}, + 'trayEnable' : true, + 'dockEnable' : true, + 'dateLastStarted' : '', + 'openBrowser' : true, + 'openBrowserURL' : '', + 'profile' : '', + 'customServerFolder' : '', + 'welcomeFiles' : '', + 'maxRequests' : '', + 'exitCode' : 0, + 'rules' : [], + 'rulesFile' : '', + 'blockCFAdmin' : false, 'blockSensitivePaths' : false, 'blockFlashRemoting' : false, - 'allowedExt' : '', - 'pidfile' : '', - 'predicateFile' : '', - 'trayOptionsFile' : '', - 'SSLForceRedirect' : false, - 'HSTSEnable' : false, - 'HSTSMaxAge' : 0, + 'allowedExt' : '', + 'pidfile' : '', + 'predicateFile' : '', + 'trayOptionsFile' : '', + 'SSLForceRedirect' : false, + 'HSTSEnable' : false, + 'HSTSMaxAge' : 0, 'HSTSIncludeSubDomains' : false, - 'AJPSecret' : '' + 'AJPSecret' : '' }; } diff --git a/src/cfml/system/util/ConsolePainter.cfc b/src/cfml/system/util/ConsolePainter.cfc index 3c456b24..50d91c60 100644 --- a/src/cfml/system/util/ConsolePainter.cfc +++ b/src/cfml/system/util/ConsolePainter.cfc @@ -138,7 +138,7 @@ component singleton accessors=true { ); } catch( any e ) { - if( !(e.type contains 'interrupted') ) { + if( !(e.type contains 'interrupt') ) { systemoutput( e.message & ' ' & e.detail, 1 ); systemoutput( "#e.tagContext[1].template#: line #e.tagContext[1].line#", 1 ); rethrow; diff --git a/src/cfml/system/util/DataConverter.cfc b/src/cfml/system/util/DataConverter.cfc index 12b1871d..903d1f7c 100644 --- a/src/cfml/system/util/DataConverter.cfc +++ b/src/cfml/system/util/DataConverter.cfc @@ -36,11 +36,18 @@ component singleton { var data = isArray(rawData) ? rawData : [rawData]; return data.map((x) => { + if( isNull( x ) ) { + return [nullValue()]; + } + if(isArray(x)) return x.map((y) => { return isSimpleValue(y) ? y : cellHasFormattingEmbedded(y) ? y : serializeJSON(y)} ); if(isStruct(x)) return x.map((k,v) => { + if( isNull( v ) ) { + return; + } return isSimpleValue(v) ? v : cellHasFormattingEmbedded(v) ? v : serializeJSON(v) }); @@ -55,13 +62,18 @@ component singleton { * Use key names for structs * @data Any type of data for the table. */ - public array function generateColumnNames (required any data, string columns="" ){ + public array function generateColumnNames(required any data, string columns="" ){ var columnsArray = []; if(isSimpleValue(data)){ columnsArray = ['col_1']; } else if ( isArray(data) ){ - columnsArray = data.map((x,i) => {return 'col_' & i},true); - arguments.columns.each(function(element,index,list) { + var i=0; + for( var x in data ) { + i++; + columnsArray.append( 'col_' & i ); + } + + arguments.columns.listEach(function(element,index,list) { columnsArray[index] = element; }) } else if ( isStruct(data) ){ diff --git a/src/cfml/system/util/DiskStore.cfc b/src/cfml/system/util/DiskStore.cfc index f5d15a9a..a0285aa3 100644 --- a/src/cfml/system/util/DiskStore.cfc +++ b/src/cfml/system/util/DiskStore.cfc @@ -144,7 +144,12 @@ Description : if( isJSON( fileContents ) ) { return deserializeJSON( fileContents ); } else { - fileDelete( thisFilePath ); + try { + fileDelete( thisFilePath ); + } catch( any e ) { + // If the file didn't exist, ignore it. This can happen + // when to CommandBox instances start at the same time. + } } } diff --git a/src/cfml/system/util/InteractiveJob.cfc b/src/cfml/system/util/InteractiveJob.cfc index 573c165c..64ae9650 100644 --- a/src/cfml/system/util/InteractiveJob.cfc +++ b/src/cfml/system/util/InteractiveJob.cfc @@ -20,7 +20,7 @@ component accessors=true singleton { property name='dumpLog' type='boolean'; property name='startTime' type='numeric'; property name='animation' type='numeric'; - + // DI property name='shell' inject='shell'; @@ -47,7 +47,7 @@ component accessors=true singleton { [ ' ◐ ', ' ◓ ', ' ◑ ', ' ◒ ' ], [ '> ', ' > ', ' >' ] ]; - + setStartTime( 0 ); setAnimation( 1 ) return this; @@ -98,13 +98,15 @@ component accessors=true singleton { // Break lines longer than the current terminal width into multiples .reduce( function( result, i ) { // Keep breaking off chunks until we're short enough to fit - while( i.len() > termWidth ) { - result.append( i.left( termWidth ) ); - i = i.right( -termWidth ); + // We need to ignore ANSI formatting when rdoing this or it will throw off the widths + while( aStr.stripAnsi( i ).length() > termWidth ) { + var attributedString = aStr.fromAnsi(i); + result.append( attributedString.subSequence( 0, termWidth-1 ).toString() ); + i = attributedString.subSequence( termWidth-1, attributedString.length()-1 ); } // Add any remaining. - if( i.len() ) { - result.append( i ); + if( i.length() ) { + result.append( i.toString() ); } return result; }, [] ) @@ -260,11 +262,11 @@ component accessors=true singleton { * @finalOutput True if getting final output at the completion of the job. */ array function getLines( job, includeAllLogs=false, finalOutput=false ) { - + if( !getActive() ) { return []; } - + if( isNull( arguments.job ) ) { if( !getJobs().len() ) { throw( 'No active job' ); @@ -285,12 +287,12 @@ component accessors=true singleton { } if( job.status == 'Running' || includeAllLogs || ( finalOutput && job.dumpLog ) ) { - + // If we're only showing one line of job logs, don't bother with the ------ dividers if( job.logSize > 1 ) { lines.append( aStr.fromAnsi( print.text( ' |' & repeatString( '-', min( job.name.len()+15, safeWidth-5 ) ), statusColor( job ) ) ) ); } - + var relevantLogLines = []; var thisLogLines = job.logLines; var thisLogSize = job.logSize; @@ -353,7 +355,7 @@ component accessors=true singleton { return print.text( '#runningAnimation()#| ' & job.name, statusColor( job ) ); } } - + /** * Returns a character to aninmate for running jobs * @@ -363,7 +365,7 @@ component accessors=true singleton { // How long has this job been running in ms? var runningTime = ( getTickCount() ) - getStartTime(); // Removing the amount of time it takes to completely cycle through the chars an even amount of time, how much is left in the current cycle? - runningTime = runningTime % ( thisRunningAnimationChars.len() * 500 ); + runningTime = runningTime % ( thisRunningAnimationChars.len() * 500 ); // Which char are we on at 500ms per char? return thisRunningAnimationChars[ ( runningTime \ 500 ) + 1 ] } @@ -434,7 +436,7 @@ component accessors=true singleton { /** * Get number that represents the depth of the currently executing job. */ - private numeric function getCurrentJobDepth() { + numeric function getCurrentJobDepth() { var pointer = getJobs(); var depth = 0; if( !pointer.len() ) { diff --git a/src/cfml/system/util/Print.cfc b/src/cfml/system/util/Print.cfc index 03f8f2c7..74707932 100644 --- a/src/cfml/system/util/Print.cfc +++ b/src/cfml/system/util/Print.cfc @@ -88,11 +88,13 @@ component { // TODO: Actually use a string buffer var ANSIString = ""; + + var foundANSI = false; // Text needing formatting var text = arrayLen(missingMethodArguments) ? missingMethodArguments[ 1 ] : ''; // Convert complex values to a string representation - if( isXML( text ) ) { + if( ( !isSimpleValue( text ) || ( left(text,1) == '<' || trim( text ).left(1) == '<' ) ) && isXML( text ) ) { text = formatterUtil.formatXML( text ); } else if( !isSimpleValue( text ) ) { diff --git a/src/cfml/system/util/PrintBuffer.cfc b/src/cfml/system/util/PrintBuffer.cfc index 399dbee0..484974b9 100644 --- a/src/cfml/system/util/PrintBuffer.cfc +++ b/src/cfml/system/util/PrintBuffer.cfc @@ -57,9 +57,11 @@ component accessors="true" extends="Print"{ // Proxy through any methods to the actual print helper function onMissingMethod( missingMethodName, missingMethodArguments ){ - // Don't modify the buffer if it's being printed - lock name='printBuffer-#getObjectID()#' type="readonly" timeout="20" { - variables.result.append( super.onMissingMethod( arguments.missingMethodName, arguments.missingMethodArguments ) ); + var result = super.onMissingMethod( arguments.missingMethodName, arguments.missingMethodArguments ); + + // Don't modify the buffer if it's being printed, exclusive because StringBuilder is not thread-safe + lock name='printBuffer-#getObjectID()#' type="exclusive" timeout="20" { + variables.result.append( result ); return this; } } diff --git a/src/cfml/system/util/ReaderFactory.cfc b/src/cfml/system/util/ReaderFactory.cfc index 795883d0..66766042 100644 --- a/src/cfml/system/util/ReaderFactory.cfc +++ b/src/cfml/system/util/ReaderFactory.cfc @@ -60,6 +60,8 @@ component singleton{ } // The JANSI lib will pick this up and use it systemSettings.setSystemProperty( 'library.jansi.path', JANSI_path ); + // https://github.com/fusesource/jansi/blob/2cf446182c823a4c110411b765a1f0367eb8a913/src/main/java/org/fusesource/jansi/internal/JansiLoader.java#L80 + systemSettings.setSystemProperty( 'jansi.tmpdir', JANSI_path ); // And JNA will pick this up. // https://java-native-access.github.io/jna/4.2.1/com/sun/jna/Native.html#getTempDir-- systemSettings.setSystemProperty( 'jna.tmpdir', JANSI_path ); diff --git a/src/cfml/system/util/SystemSettings.cfc b/src/cfml/system/util/SystemSettings.cfc index 78fecf06..6445b4ca 100644 --- a/src/cfml/system/util/SystemSettings.cfc +++ b/src/cfml/system/util/SystemSettings.cfc @@ -213,7 +213,11 @@ component singleton { // Loop over and process each key for( var key in dataStructure ) { var expandedKey = expandSystemSettings( key, context ); - dataStructure[ expandedKey ] = expandDeepSystemSettings( dataStructure[ key ], context ); + if( isNull( dataStructure[ key ] ) ) { + dataStructure[ expandedKey ] = nullValue(); + } else { + dataStructure[ expandedKey ] = expandDeepSystemSettings( dataStructure[ key ], context ); + } if( expandedKey != key ) dataStructure.delete( key ); } return dataStructure; @@ -223,7 +227,9 @@ component singleton { // Loop over and process each index for( var item in dataStructure ) { i++; - dataStructure[ i ] = expandDeepSystemSettings( item, context ); + if( !isNull( item ) ) { + dataStructure[ i ] = expandDeepSystemSettings( item, context ); + } } return dataStructure; // If it's a string... diff --git a/src/cfml/system/util/TablePrinter.cfc b/src/cfml/system/util/TablePrinter.cfc index a7edf8ca..f47f75a2 100644 --- a/src/cfml/system/util/TablePrinter.cfc +++ b/src/cfml/system/util/TablePrinter.cfc @@ -15,6 +15,7 @@ component { property name="print" inject="PrintBuffer"; property name="shell" inject="shell"; property name="convert" inject="DataConverter"; + property name="job" inject="InteractiveJob"; variables.tableChars = { "top": chr( 9552 ), // ═ @@ -119,7 +120,12 @@ component { var headerData = arguments.headers.map( ( header, index ) => calculateColumnData( index, header, data, headerNames ), true ); var termWidth = arguments.width; if( termWidth <= 0 ) { - termWidth = shell.getTermWidth()-1; + // If this table is going to get captured in job output, ensure it will fix based on the job depth + if( job.getActive() ) { + termWidth = shell.getTermWidth()-2-( job.getCurrentJobDepth() * 4 ); + } else { + termWidth = shell.getTermWidth()-1; + } } if( termWidth <= 0 ) { termWidth = 100; diff --git a/src/cfml/system/wirebox/system/cache/providers/CacheBoxProvider.cfc b/src/cfml/system/wirebox/system/cache/providers/CacheBoxProvider.cfc index 1f734c68..57cf839d 100644 --- a/src/cfml/system/wirebox/system/cache/providers/CacheBoxProvider.cfc +++ b/src/cfml/system/wirebox/system/cache/providers/CacheBoxProvider.cfc @@ -538,7 +538,7 @@ component lock type="exclusive" name="CacheBoxProvider.reap.#variables.cacheId#" timeout="#variables.lockTimeout#"{ // log it - variables.logger.info( "Starting to reap CacheBoxProvider: #getName()#, id: #variables.cacheId#" ); + variables.logger.debug( "Starting to reap CacheBoxProvider: #getName()#, id: #variables.cacheId#" ); // Run Storage reaping first, before our local algorithm variables.objectStore.reap(); @@ -601,7 +601,7 @@ component } // log it - variables.logger.info( "Finished reap in #getTickCount()-sTime#ms for CacheBoxProvider: #getName()#, id: #variables.cacheId#" ); + variables.logger.debug( "Finished reap in #getTickCount()-sTime#ms for CacheBoxProvider: #getName()#, id: #variables.cacheId#" ); return this; } diff --git a/src/java/cliloader/LoaderCLIMain.java b/src/java/cliloader/LoaderCLIMain.java index a530d1d5..a1d7336c 100644 --- a/src/java/cliloader/LoaderCLIMain.java +++ b/src/java/cliloader/LoaderCLIMain.java @@ -292,7 +292,7 @@ && new File( cliArguments.get( 0 ) ).isFile() ) { // Escape backslash in webroot since replace uses a regular expression // The bootstrap is the first .cfm file we will cfinclude from the "webroot" String bootstrap = "/" + Paths.get( uri ).toAbsolutePath().toString().replaceFirst( webroot.replace( "\\", "\\\\" ), "" ); - + // contextroot sets lucee's "webroot" inside the scripting engine to be our drive root System.setProperty( "lucee.cli.contextRoot", webroot ); // These next two are the Lucee web context and server context homes @@ -526,7 +526,7 @@ public static boolean listContains( ArrayList< String > argList, String text ){ public static int listIndexOf( ArrayList< String > argList, String text ){ int index = 0; for( String item : argList) { - if( item.toLowerCase().startsWith( text.toLowerCase() ) + if( item.toLowerCase().startsWith( text.toLowerCase() ) || item.toLowerCase().startsWith( "-" + text.toLowerCase() ) ) { return index; }