diff --git a/build/build.properties b/build/build.properties index ec424298..0da6631d 100644 --- a/build/build.properties +++ b/build/build.properties @@ -10,16 +10,17 @@ java.debug=true #dependencies dependencies.dir=${basedir}/lib -cfml.version=5.3.10.97 +cfml.version=5.3.10.120 cfml.extensions=8D7FB0DF-08BB-1589-FE3975678F07DB17 -cfml.loader.version=2.8.1 +box.bunndled.modules=commandbox-update-check,commandbox-cfconfig,commandbox-dotenv +cfml.loader.version=2.8.3 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.17+8 +jre.version=jdk-11.0.18+10 launch4j.version=3.14 -runwar.version=4.7.16 +runwar.version=4.8.3 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 d39038e5..5fd7c115 100644 --- a/build/build.xml +++ b/build/build.xml @@ -16,8 +16,8 @@ External Dependencies: - - + + @@ -297,8 +297,7 @@ External Dependencies: - - + @@ -337,7 +336,6 @@ External Dependencies: - @@ -406,6 +404,45 @@ External Dependencies: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -703,6 +740,14 @@ External Dependencies: + + + + + + + + diff --git a/src/cfml/system/BaseCommand.cfc b/src/cfml/system/BaseCommand.cfc index 2fc576e0..d13b325e 100644 --- a/src/cfml/system/BaseCommand.cfc +++ b/src/cfml/system/BaseCommand.cfc @@ -221,9 +221,9 @@ component accessors="true" singleton { function error( required message, detail='', clearPrintBuffer=false, exitCode=1 ) { wirebox.getInstance( "ConsolePainter" ).stop( message ); - + if( !getSystemSetting( 'box_currentCommandPiped', false ) ) { - print.line().toConsole(); + print.line().toConsole(); } setExitCode( arguments.exitCode ); @@ -234,6 +234,25 @@ component accessors="true" singleton { throw( message=arguments.message, detail=arguments.detail, type="commandException", errorcode=arguments.exitCode ); } + /** + * This method mimics a Java/Groovy assert() function, where it evaluates the target to a boolean value or an executable closure and it must be true + * to pass and return a true to you, or throw an `AssertException` + * + * @target The tareget to evaluate for being true, it can also be a closure that will be evaluated at runtime + * @message The message to send in the exception + * + * @throws AssertException if the target is a false or null value + * @return True, if the target is a non-null value. If false, then it will throw the `AssertError` exception + */ + boolean function assert( target, message="" ){ + // param against nulls + arguments.target = arguments.target ?: false; + // evaluate it + var results = isClosure( arguments.target ) || isCustomFunction( arguments.target ) ? arguments.target( this ) : arguments.target; + // deal it : callstack two is from where the `assert` was called. + return results ? true : error( "Assertion failed from #callStackGet()[ 2 ].toString()#", arguments.message ); + } + /** * Tells you if the error() method has been called on this command. **/ @@ -348,5 +367,22 @@ component accessors="true" singleton { return getCurrentThread().getName(); } + /** + * Install an extension into the Lucee server instance inside the CLI. + * If the extension is already installed, nothing will happen + * + * @extensionID The ID of the extenstion to install into the CLI + * @extensionVersion The version of the extension to install into the CLI + * @LuceeContextType Either "server" or "web" + * @LuceeContextPassword Use this if you've changed the default Lucee context password in the CLI + */ + function installExtension( + required string extensionID, + string extensionVersion='latest', + string luceeContextType='server', + string luceeContextPassword='commandbox' + ){ + new Administrator( luceeContextType, luceeContextPassword ).updateExtension( extensionID, extensionVersion ); + } } diff --git a/src/cfml/system/Quotes.txt b/src/cfml/system/Quotes.txt index 83374c34..673946d3 100644 --- a/src/cfml/system/Quotes.txt +++ b/src/cfml/system/Quotes.txt @@ -13,7 +13,7 @@ Forget a server as you stop it with "stop --forget" Stop all your servers at once with "stop --all" View the servers for a given folder with "server list --local" This CLI viewed best on Netscape navigator 2.0 -On WIndows? Use ConEMU, it supports 256 glorious colors! +On Windows? Use ConEMU, it supports 256 glorious colors! The code name for CommandBox before it launched was "Project Gideon" CommandBox is a registered trademark of Ortus Solutions CommandBox is professional supported open source software diff --git a/src/cfml/system/Shell.cfc b/src/cfml/system/Shell.cfc index b7a258d3..52acff88 100644 --- a/src/cfml/system/Shell.cfc +++ b/src/cfml/system/Shell.cfc @@ -171,6 +171,21 @@ component accessors="true" singleton { fileWrite( systemBoxJSON, '{ "name":"CommandBox System" }' ); } + // Merge in any auto-installed modules + var systemBoxJSONAutoInstall = expandPath( '/commandbox/box-auto-install.json' ); + if( fileExists( systemBoxJSONAutoInstall ) ) { + var boxJSON = deserializeJSON( fileRead( systemBoxJSON ) ); + var boxJSONAuto = deserializeJSON( fileRead( systemBoxJSONAutoInstall ) ); + boxJSON.dependencies = boxJSON.dependencies ?: {}; + boxJSON.installPaths = boxJSON.installPaths ?: {}; + boxJSONAuto.dependencies = boxJSONAuto.dependencies ?: {}; + boxJSONAuto.installPaths = boxJSONAuto.installPaths ?: {}; + boxJSONAuto.dependencies.each( (k,v)=> boxJSON.dependencies[k]=v ); + boxJSONAuto.installPaths.each( (k,v)=> boxJSON.installPaths[k]=v ); + fileWrite( systemBoxJSON, serializeJSON( boxJSON ) ); + fileDelete( systemBoxJSONAutoInstall ); + } + } diff --git a/src/cfml/system/box.json b/src/cfml/system/box.json index 0b7168c2..0d8a7acf 100644 --- a/src/cfml/system/box.json +++ b/src/cfml/system/box.json @@ -1,22 +1,24 @@ { - "name": "CommandBox System Core", - "version": "@build.version@+@build.number@", - "author": "Brad Wood", - "shortDescription": "This tracks the CommandBox core dependencies", - "dependencies": { - "string-similarity": "^1.0.0", - "semver": "^1.2.3", - "globber": "^3.0.4", - "JSONPrettyPrint": "^1.0.0", - "propertyFile": "^1.0.9", - "JMESPath": "^2.4.0" - }, - "devDependencies": {}, - "installPaths": { - "string-similarity": "modules\\string-similarity", - "semver": "modules/semver/", - "globber": "modules/globber/", - "JSONPrettyPrint": "modules\\JSONPrettyPrint", - "propertyFile": "modules\\propertyFile" - } + "name":"CommandBox System Core", + "version":"@build.version@+@build.number@", + "author":"Brad Wood", + "shortDescription":"This tracks the CommandBox core dependencies", + "dependencies":{ + "string-similarity":"^1.0.0", + "semver":"^1.2.3", + "globber":"^3.0.4", + "JSONPrettyPrint":"^1.0.0", + "propertyFile":"^1.0.9", + "JMESPath":"^2.4.0", + "jsondiff":"^1.1.3" + }, + "devDependencies":{}, + "installPaths":{ + "string-similarity":"modules\\string-similarity", + "semver":"modules/semver/", + "globber":"modules/globber/", + "JSONPrettyPrint":"modules\\JSONPrettyPrint", + "propertyFile":"modules\\propertyFile", + "jsondiff":"modules/jsondiff/" + } } \ No newline at end of file diff --git a/src/cfml/system/config/box.json.txt b/src/cfml/system/config/box.json.txt index f8f24a81..9ce9168e 100644 --- a/src/cfml/system/config/box.json.txt +++ b/src/cfml/system/config/box.json.txt @@ -75,5 +75,8 @@ "watchDelay":500, "watchPaths":"**.cfc", "options":{} - } + }, + "reinitWatchDirectory" : "", + "reinitWatchDelay" : 500, + "reinitWatchPaths" : "", } diff --git a/src/cfml/system/config/server.schema.json b/src/cfml/system/config/server.schema.json index eb6251ac..bf9fb692 100644 --- a/src/cfml/system/config/server.schema.json +++ b/src/cfml/system/config/server.schema.json @@ -81,6 +81,12 @@ "type": "string", "default": "" }, + "preferredBrowser": { + "title": "Preferred Browser", + "description": "Preferred Browser to use for open commands, including 'server open' and the tray menus", + "type": "string", + "default": "" + }, "openBrowser": { "title": "Open Browser", "description": "Controls whether browser opens by default when starting server", @@ -886,6 +892,11 @@ "description": "Runs when engine is being installed during server startup", "type": "string" }, + "onServerInitialInstall": { + "title": "On Server Initial Install Script", + "description": "Runs when engine is being installed the first time during server startup", + "type": "string" + }, "onServerStop": { "title": "On Server Stop Script", "description": "Runs before a server stop", diff --git a/src/cfml/system/endpoints/Git.cfc b/src/cfml/system/endpoints/Git.cfc index 5fb94eaf..76fd04d2 100644 --- a/src/cfml/system/endpoints/Git.cfc +++ b/src/cfml/system/endpoints/Git.cfc @@ -47,7 +47,7 @@ component accessors="true" implements="IEndpoint" singleton { public string function resolvePackage( required string package, boolean verbose=false ) { if( configService.getSetting( 'offlineMode', false ) ) { - throw( 'Can''t clone [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); + throw( 'Can''t clone [#getNamePrefixes()#:#package#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'endpointException' ); } var job = wirebox.getInstance( 'interactiveJob' ); @@ -103,6 +103,9 @@ component accessors="true" implements="IEndpoint" singleton { if( branchList.filter( function( i ) { return i contains branch; } ).len() ) { if( arguments.verbose ){ job.addLog( 'Commit-ish [#branch#] appears to be a branch.' ); } branch = 'origin/' & branch; + } else if( branch == 'master' && branchList.filter( (b)=>b contains 'main' ).len() ) { + if( arguments.verbose ){ job.addLog( 'Switching to branch [main].' ); } + branch = 'origin/main'; } // Checkout branch, tag, or commit hash. diff --git a/src/cfml/system/modules/globber/box.json b/src/cfml/system/modules/globber/box.json index 19826fda..93652410 100644 --- a/src/cfml/system/modules/globber/box.json +++ b/src/cfml/system/modules/globber/box.json @@ -1,6 +1,6 @@ { "name":"Globber", - "version":"3.1.4", + "version":"3.1.5", "author":"Brad Wood", "homepage":"https://github.com/Ortus-Solutions/globber/", "documentation":"https://github.com/Ortus-Solutions/globber/", diff --git a/src/cfml/system/modules/jsondiff/LICENSE b/src/cfml/system/modules/jsondiff/LICENSE new file mode 100644 index 00000000..51fdf697 --- /dev/null +++ b/src/cfml/system/modules/jsondiff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Scott Steinbeck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/cfml/system/modules/jsondiff/ModuleConfig.cfc b/src/cfml/system/modules/jsondiff/ModuleConfig.cfc new file mode 100644 index 00000000..e141cf03 --- /dev/null +++ b/src/cfml/system/modules/jsondiff/ModuleConfig.cfc @@ -0,0 +1,16 @@ +component { + + this.name = 'JSON-Diff'; + this.title = 'JSON-Diff'; + this.author = 'Scott Steinbeck'; + this.webURL = 'https://github.com/scottsteinbeck/json-diff'; + this.description = 'An ColdFusion utility for checking if 2 JSON objects have differences'; + this.version = '1.0.4'; + this.autoMapModels = false; + this.dependencies = []; + + function configure() { + binder.map('jsondiff').to('#moduleMapping#.models.jsondiff'); + } + +} \ No newline at end of file diff --git a/src/cfml/system/modules/jsondiff/box.json b/src/cfml/system/modules/jsondiff/box.json new file mode 100644 index 00000000..a3b9ac3a --- /dev/null +++ b/src/cfml/system/modules/jsondiff/box.json @@ -0,0 +1,46 @@ +{ + "name":"JSON-Diff", + "slug":"jsondiff", + "version":"1.1.3", + "License":[ + { + "type":"MIT", + "URL":"https://github.com/scottsteinbeck/json-diff/blob/master/LICENSE" + } + ], + "author":"Scott Steinbeck ", + "Documentation":"https://github.com/scottsteinbeck/json-diff", + "Repository":{ + "type":"git", + "URL":"https://github.com/scottsteinbeck/json-diff" + }, + "scripts":{ + "postVersion":"publish", + "onRelease":"!git push --follow-tags" + }, + "Bugs":"https://github.com/scottsteinbeck/json-diff/issues", + "shortDescription":"An ColdFusion utility for checking if 2 JSON objects have differences", + "keywords":[ + "json diff", + "json-diff", + "json differences" + ], + "private":"false", + "engines":[ + { + "type":"lucee", + "version":">=4.5.x" + } + ], + "Contributors":[ + "Scott Steinbeck " + ], + "DevDependencies":{ + "testbox":"^4.2.1+400" + }, + "installPaths":{ + "testbox":"testbox/" + }, + "type":"modules", + "dependencies":{} +} \ No newline at end of file diff --git a/src/cfml/system/modules/jsondiff/models/jsondiff.cfc b/src/cfml/system/modules/jsondiff/models/jsondiff.cfc new file mode 100644 index 00000000..c284fdbd --- /dev/null +++ b/src/cfml/system/modules/jsondiff/models/jsondiff.cfc @@ -0,0 +1,288 @@ +component singleton { + variables.uniqueKeyName = '________key'; + function numericCheck(value) { + if ( + getMetadata(value).getName() == 'java.lang.Double' || + getMetadata(value).getName() == 'java.lang.Integer' + ) + return true; + return false + } + + function isSame(first, second) { + if (isNull(first) && isNull(second)) return true; + if (isNull(first) || isNull(second)) return false; + if (isSimpleValue(first) && isSimpleValue(second)) { + if (numericCheck(first) && numericCheck(second) && precisionEvaluate(first - second) != 0) { + return false; + } else if (first != second) { + return false; + } + return true; + } + + // We know that first and second have the same type so we can just check the + // first type from now on.1 + if (isArray(first) && isArray(second)) { + // Short circuit if they're not the same length; + if (first.len() != second.len()) { + return false; + } + for (var i = 1; i <= first.len(); i++) { + if (isSame(first[i], second[i]) == false) { + return false; + } + } + return true; + } + + if (isStruct(first) && isStruct(second)) { + // echo('we are here') + // An object is equal if it has the same key/value pairs. + var keysSeen = {}; + for (var key in first) { + // echo('first -> ' & key & '
'); + if (structKeyExists(first, key) && structKeyExists(second, key)) { + if (isSame(first[key], second[key]) == false) { + return false; + } + keysSeen[key] = true; + } + } + // Now check that there aren't any keys in second that weren't + // in first. + for (var key2 in second) { + // echo('second -> ' & key2 & '
'); + if (!structKeyExists(second, key2) || !structKeyExists(keysSeen, key2)) { + return false; + } + } + return true; + } + return false; + } + + function groupData(required array data, required array uniqueKeys) { + return data.reduce((acc, x) => { + var uniqueKey = uniqueKeys.reduce((accKey, key) => { + accKey.append(x[key]); + return accKey; + }, []); + uniqueKey = serializeJSON(uniqueKey); + x[variables.uniqueKeyName] = uniqueKey; + acc[uniqueKey] = x; + return acc + }, {}) + } + + + function diffByKey( + array first = [], + array second = [], + required any uniqueKeys, + array ignoreKeys = [] + ) { + + if (!isArray(uniqueKeys)) { + uniqueKeys = [uniqueKeys]; + } + var data1 = groupData(first, uniqueKeys); + var data2 = groupData(second, uniqueKeys); + var diffData = diff(data1, data2, ignoreKeys); + var groupedDiff = diffData.reduce((acc, x) => { + if (x.type == 'add') { + key = x.new[variables.uniqueKeyName]; + x.new.delete(variables.uniqueKeyName) + acc[x.type].append({'key': deserializeKey(key), 'data': x.new}); + } else if (x.type == 'remove') { + key = x.old[variables.uniqueKeyName]; + x.old.delete(variables.uniqueKeyName) + acc[x.type].append({'key': deserializeKey(key), 'data': x.old}); + } else if (x.type == 'change') { + if (!acc[x.type].keyExists(x.path[1])) acc[x.type][x.path[1]] = []; + var pathRest = arraySlice(x.path, 2); + acc[x.type][x.path[1]].append({ + 'key': pathRest[1], + 'path': pathRest, + 'new': x.new, + 'old': x.old + }); + } + return acc + }, {'add': [], 'remove': [], 'change': {}}); + groupedDiff['update'] = groupedDiff.change.reduce((acc, key, value) => { + data1[key].delete(variables.uniqueKeyName); + data2[key].delete(variables.uniqueKeyName); + acc.push({ + 'key': deserializeKey(key), + 'orig': data1[key], + 'data': data2[key], + 'changes': value + }) + return acc; + }, []); + groupedDiff.delete('change'); + first.map((row) => { row.delete(variables.uniqueKeyName)}) + second.map((row) => { row.delete(variables.uniqueKeyName)}) + return groupedDiff; + } + + function deserializeKey(serializedKey){ + var valueArr = deserializeJSON(serializedKey); + if(valueArr.len() == 1) return valueArr[1]; + return valueArr; + } + + // Now check that there aren't any keys in second that weren't + function diff(any first = '', any second = '', array ignoreKeys = []) { + var diffs = []; + if ( + (isSimpleValue(first) && !isSimpleValue(second)) + || (!isSimpleValue(first) && isSimpleValue(second)) + ) { + diffs.append({ + 'path': [], + 'type': 'CHANGE', + 'old': first, + 'new': second + }); + } else if (isSimpleValue(first) && isSimpleValue(second)) { + if ( + numericCheck(first) + && numericCheck(second) + ) { + if (precisionEvaluate(first - second) != 0) { + diffs.append({ + 'path': [], + 'type': 'CHANGE', + 'old': first, + 'new': second + }); + } + } else if (first != second) { + diffs.append({ + 'path': [], + 'type': 'CHANGE', + 'old': first, + 'new': second + }); + } + } else if (isArray(first) && isArray(second)) { + for (var i = 1; i <= first.len(); i++) { + var path = i; + + if (second.len() < i) { + diffs.append({ + 'path': [path], + 'type': 'REMOVE', + 'old': first[i], + 'new': '' + }); + } else if (isSimpleValue(first[i]) && isSimpleValue(second[i])) { + if ( + numericCheck(first[i]) + && numericCheck(second[i]) + ) { + if (precisionEvaluate(first[i] - second[i]) != 0) { + diffs.append({ + 'path': [path], + 'type': 'CHANGE', + 'old': first[i], + 'new': second[i] + }); + } + } else if (first[i] != second[i]) { + diffs.append({ + 'path': [path], + 'type': 'CHANGE', + 'old': first[i], + 'new': second[i] + }); + } + } else { + var nestedDiffs = diff(first[i], second[i], ignoreKeys); + nestedDiffs = nestedDiffs.each((difference) => { + difference.path.prepend(path); + diffs.append(difference); + }); + } + } + for (var t = first.len() + 1; t <= second.len(); t++) { + var path = t; + diffs.append({ + 'type': 'ADD', + 'path': [path], + 'old': '', + 'new': second[path] + }); + } + } else if (isStruct(first) && isStruct(second)) { + var keysSeen = {}; + for (var key in first) { + var path = key; + if (ignoreKeys.find(key) > 0) { + continue; + } + if (!first.keyExists(key)) first[key] = ''; + if (!second.keyExists(key)) { + diffs.append({ + 'path': [path], + 'type': 'REMOVE', + 'old': first[key], + 'new': '' + }); + } else if (isSimpleValue(first[key]) && isSimpleValue(second[key])) { + if ( + numericCheck(first[key]) + && numericCheck(second[key]) + ) { + if (precisionEvaluate(first[key] - second[key]) != 0) { + diffs.append({ + 'key': path, + 'path': [path], + 'type': 'CHANGE', + 'old': first[key], + 'new': second[key] + }); + } + } else if (first[key] != second[key]) { + diffs.append({ + 'key': path, + 'path': [path], + 'type': 'CHANGE', + 'old': first[key], + 'new': second[key] + }); + } + } else { + if (structKeyExists(first, key) && structKeyExists(second, key)) { + var nestedDiffs = diff(first[key], second[key], ignoreKeys); + nestedDiffs = nestedDiffs.each((difference) => { + difference.path.prepend(path); + diffs.append(difference); + }) + } + } + keysSeen[key] = true; + } + // Now check that there aren't any keys in second that weren't + // in first. + for (var key2 in second) { + if (ignoreKeys.find(key2) > 0) { + continue; + }; + if (structKeyExists(second, key2) && !structKeyExists(keysSeen, key2)) { + diffs.append({ + 'type': 'ADD', + 'path': [key2], + 'old': '', + 'new': second[key2] + }); + } + } + } + + return diffs; + } + +} diff --git a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/watch-reinit.cfc b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/watch-reinit.cfc index 7e37974f..c8abf1a4 100644 --- a/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/watch-reinit.cfc +++ b/src/cfml/system/modules_app/coldbox-commands/commands/coldbox/watch-reinit.cfc @@ -17,7 +17,8 @@ * * {code} * package set reinitWatchDelay=1000 - * package set reinitWatchPaths= "config/**.cfc,handlers/**.cfc,models/**.cfc,ModuleConfig.cfc" + * package set reinitWatchPaths="config/**.cfc,handlers/**.cfc,models/**.cfc,ModuleConfig.cfc" + * package set reinitWatchDirectory="../" * {code} * * This command will run in the foreground until you stop it. When you are ready to shut down the watcher, press Ctrl+C. @@ -36,11 +37,13 @@ component { * @paths Command delimited list of file globbing paths to watch relative to the working directory, defaults to **.cfc * @delay How may milliseconds to wait before polling for changes, defaults to 500 ms * @password Reinit password + * @directory Working directory to start watcher in **/ function run( string paths, number delay, - string password = "1" + string password = "1", + string directory ){ // Get watch options from package descriptor var boxOptions = packageService.readPackageDescriptor( getCWD() ); @@ -59,8 +62,12 @@ component { // Determine watching patterns, either from arguments or boxoptions or defaults var globbingPaths = arguments.paths ?: getOptionsWatchers() ?: variables.PATHS; - // handle non numeric config and put a floor of 150ms - var delayMs = max( val( arguments.delay ?: boxOptions.reinitWatchDelay ?: variables.WATCH_DELAY ), 150 ); + var globArray = globbingPaths.listToArray(); + var theDirectory = arguments.directory ?: boxOptions.reinitWatchDirectory ?: getCWD(); + theDirectory = resolvePath( theDirectory ); + + // handle non numeric config + var delayMs = max( val( arguments.delay ?: boxOptions.reinitWatchDelay ?: variables.WATCH_DELAY ), variables.WATCH_DELAY ); var statusColors = { "added" : "green", "removed" : "red", @@ -92,15 +99,19 @@ component { .greenLine( "---------------------------------------------------" ) .greenLine( "Watching the following files for a framework reinit" ) .greenLine( "---------------------------------------------------" ) - .greenLine( " " & globbingPaths ) + .line(); + globArray.each( (p) => print.greenLine( " " & p ) ); + print + .line() + .greenLine( " in directory: #theDirectory#" ) .greenLine( " Press Ctrl-C to exit " ) .greenLine( "---------------------------------------------------" ) .toConsole(); // Start watcher watch() - .paths( globbingPaths.listToArray() ) - .inDirectory( getCWD() ) + .paths( globArray ) + .inDirectory( theDirectory ) .withDelay( delayMs ) .onChange( function( changeData ){ // output file changes diff --git a/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/use.cfc b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/use.cfc index a1156be0..07ff9294 100644 --- a/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/use.cfc +++ b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/use.cfc @@ -12,8 +12,9 @@ **/ component { - property name="configService" inject="configService"; - property name="endpointService" inject="endpointService"; + property name="configService" inject="configService"; + property name="endpointService" inject="endpointService"; + property name='interceptorService' inject='interceptorService'; /** * @username The ForgeBox username to switch to. @@ -55,6 +56,8 @@ component { } else { error( 'Username [#arguments.username#] isn''t authenticated. Please use "forgebox login".' ); } + + interceptorService.announceInterception( 'onEndpointLogin', { endpointName=endpointName, username=username, endpoint=oEndpoint, APIToken=APIToken } ); } function endpointNameComplete() { diff --git a/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/version-debug.cfc b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/version-debug.cfc new file mode 100644 index 00000000..593b31d6 --- /dev/null +++ b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/version-debug.cfc @@ -0,0 +1,215 @@ +/** + * Debug what semantic version range will match on ForgeBox. Helpful to test before actually installing + * Provide a ForgeBox install ID in the form of package@version and find out what version of the package that would actually install + * . + * {code:bash} + * forgebox version-debug coldbox@5.x + * {code} + * . + **/ +component { + + // DI + property name="semanticVersion" inject="semanticVersion@semver"; + property name="endpointService" inject="endpointService"; + property name="configService" inject="configService"; + + /** + * @installID Install ID to test + * @installID.optionsUDF IDComplete + * @endpointName Name of endpoint (defaults to "forgebox") + * @showMatchesOnly True will filter out display of package versions that didnt match sem ver range + **/ + function run( required string installID, string endpointName, boolean showMatchesOnly=false ) { + endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + + try { + var oEndpoint = endpointService.getEndpoint( endpointName ); + } catch( EndpointNotFound var e ) { + error( e.message, e.detail ?: '' ); + } + + var APIToken = oEndpoint.getAPIToken(); + var forgebox = oEndpoint.getForgeBox(); + + var slug = oEndpoint.parseSlug( installID ) + var version = oEndpoint.parseVersion( installID ) + var satisfyingVersion = '' + print.line( 'Endpoint: #endpointName#' ) + .line( 'Requested Slug: #slug#' ) + .line( 'Requested Version: #version#' ) + .line(); + try { + + print.yellowLine( "Verifying package '#slug#' in #endpointName#, please wait..." ).toConsole(); + + var entryData = forgebox.getEntry( slug, APIToken ); + + + print.yellowLine( 'Package [#slug#] has #entryData.versions.len()# versions.' ).line().toConsole(); + + if( !entryData.isActive ) { + error( 'The #endpointName# entry [#entryData.title#] is inactive.', 'endpointException' ); + } + + var versions = entryData.versions.map( (v)=>v.version ); + var matches = []; + // If this is an exact version (not a range) just do a simple lookup for it + if( semanticVersion.isExactVersion( version, true ) ) { + print.line( 'Requested version [#version#] is an exact version, so no semantic version ranges are begin used, just a direct match.' ) + for( var thisVer in versions ) { + if( semanticVersion.isEQ( version, thisVer, true ) ) { + matches.append( thisVer.version ); + print.line( 'Exact match [#thisVer#] found.' ) + satisfyingVersion = thisVer; + break; + } + } + if( !len( satisfyingVersion ) ) { + print.redLine( 'Exact version [#version#] not found for package [#slug#].' ); + } + } else { + + print.line( 'Requested version [#version#] is a semantic range, so searching against #versions.len()# versions for matches.' ) + // For version ranges, do a smart lookup + versions.sort( function( a, b ) { return semanticVersion.compare( b, a ) } ); + for( var thisVersion in versions ) { + if( semanticVersion.satisfies( thisVersion, version ) ) { + matches.append( thisVersion ); + } + } + + if( matches.len() ) { + satisfyingVersion = matches[1]; + print.line( 'Found #matches.len()# matches for our version range, so taking the latest one.' ); + } else if( version == 'stable' && arrayLen( versions ) ) { + print.line( "The version [stable] doesn't match any avaialble versions, which means all versions are a pre-release, so we'll just grab the latest one (same as [be])." ) + satisfyingVersion = versions[ 1 ]; + + matches = v + } else { + print.redLine( 'Version [#version#] not found for package [#slug#].' ); + } + } + + if( len( satisfyingVersion ) ) { + print.line().boldGreenline( "Version [#satisfyingVersion#] would be chosen for installation." ); + } + + } catch( forgebox var e ) { + + if( e.detail contains 'The entry slug sent is invalid or does not exist' ) { + error( "#e.message# #e.detail#" ); + } + + print.redline( "Aww man, #endpointName# ran into an issue."); + error( "#e.message# #e.detail#" ); + + } + + print.line() + .line(); + + if( showMatchesOnly ) { + versions = matches; + } + // Create table that matches screen width and outputs versions from "lowest" to "highest" down the columns from left to right + var numVersions = versions.len(); + if( numVersions ) { + var versions = versions.reverse() + var widestVersion = versions.map( (v)=>len( v ) ).max(); + var colWdith = widestVersion + 4; + var termWidth = shell.getTermWidth()-1; + var numCols = termWidth\colWdith; + var numRows = ceiling( numVersions/numCols ); + + loop from=1 to=numRows index="local.row" { + loop from=1 to=numCols index="local.col" { + var thisIndex = row+((col-1)*numRows); + var format='grey'; + if( thisIndex > numVersions ) { + var thisVersion = ''; + } else { + var thisVersion = versions[thisIndex]; + if( satisfyingVersion == thisVersion ) { + format = 'boldwhiteOnGreen'; + } else if ( matches.find( thisVersion ) ) { + format = 'boldWhite'; + } + } + print.text( padRight( thisVersion, colWdith ), format ); + } + print.line() + } + print.line() + .greyLine( 'Unmatched Version' ) + .boldWhiteLine( 'Version matching semver range' ) + .boldwhiteOnGreenLine( 'Chosen Version' ); + + } + } + + function endpointNameComplete() { + return getInstance( 'endpointService' ).forgeboxEndpointNameComplete(); + } + + + // Auto-complete list of IDs + function IDComplete( string paramSoFar ) { + // Only hit forgebox if they've typed something. + if( !len( trim( arguments.paramSoFar ) ) ) { + return []; + } + try { + + + var endpointName = configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + + try { + var oEndpoint = endpointService.getEndpoint( endpointName ); + } catch( EndpointNotFound var e ) { + error( e.message, e.detail ?: '' ); + } + + var forgebox = oEndpoint.getForgebox(); + var APIToken = oEndpoint.getAPIToken(); + + // Get auto-complete options + return forgebox.slugSearch( searchTerm=arguments.paramSoFar, APIToken=APIToken ); + } catch( forgebox var e ) { + // Gracefully handle ForgeBox issues + print + .line() + .yellowLine( e.message & chr( 10 ) & e.detail ) + .toConsole(); + // After outputting the message above on a new line, but the user back where they started. + getShell().getReader().redrawLine(); + } + // In case of error, break glass. + return []; + } + + + /** + * Adds characters to the right of a string until the string reaches a certain length. + * If the text is already greater than or equal to the maxWidth, the text is returned unchanged. + * @text The text to pad. + * @maxWidth The number of characters to pad up to. + * @padChar The character to use to pad the text. + */ + private string function padRight( required string text, required numeric maxWidth, string padChar = " " ) { + var textLength = len( arguments.text ); + if ( textLength == arguments.maxWidth ) { + return arguments.text; + } else if( textLength > arguments.maxWidth ) { + if( arguments.maxWidth < 4 ) { + return left( text, arguments.maxWidth ); + } else { + return left( text, arguments.maxWidth-3 )&'...'; + } + } + arguments.text &= repeatString( arguments.padChar, arguments.maxWidth-textLength ); + return arguments.text; + } + +} diff --git a/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/whoami.cfc b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/whoami.cfc index c959077d..c0c9a69e 100644 --- a/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/whoami.cfc +++ b/src/cfml/system/modules_app/forgebox-commands/commands/forgebox/whoami.cfc @@ -13,8 +13,9 @@ component { /** * @endpointName Name of custom forgebox endpoint to use * @endpointName.optionsUDF endpointNameComplete + * @json Set true for JSON format of user data **/ - function run( string endpointName ){ + function run( string endpointName, boolean json=false ){ try { endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); @@ -32,13 +33,21 @@ component { if( endpointName == 'forgebox' ) { error( 'You don''t have a Forgebox API token set.', 'Use "forgebox login" to authenticate as a user.' ); } else { - error( 'You don''t have a Forgebox API token set.', 'Use "forgebox login endpointName=#endpointName#" to authenticate as a user.' ); + error( 'You don''t have a Forgebox API token set.', 'Use "endpoint login endpointName=#endpointName#" to authenticate as a user.' ); + } + } + var userData = forgebox.whoami( APIToken ); + if( json ) { + print.line( userData ); + } else { + print.boldLine( '#userData.fname# #userData.lname# (#userData.username#)' ) + .line( userData.email ); + if( !isNull( userData.subscription.plan ) ) { + print.line() + .line( '#userData.subscription.subscriptionType.UcFirst()# Plan: #userData.subscription.plan.name#' ) + .indentedLine( userData.subscription.plan.features ); } } - userData = forgebox.whoami( APIToken ); - - print.boldLine( '#userData.fname# #userData.lname# (#userData.username#)' ) - .line( userData.email ); } catch( forgebox var e ) { // This can include "expected" errors such as "Email already in use" diff --git a/src/cfml/system/modules_app/package-commands/commands/package/install.cfc b/src/cfml/system/modules_app/package-commands/commands/package/install.cfc index 40fcd2ec..f82a87cf 100644 --- a/src/cfml/system/modules_app/package-commands/commands/package/install.cfc +++ b/src/cfml/system/modules_app/package-commands/commands/package/install.cfc @@ -101,6 +101,8 @@ component aliases="install" { /** * @ID.hint "endpoint:package" to install. Default endpoint is "forgebox". If no ID is passed, all dependencies in box.json will be installed. * @ID.optionsUDF IDComplete + * @ID.optionsFileComplete true + * @ID.optionsDirectoryComplete true * @directory.hint The directory to install in and creates the directory if it does not exist. This will override the packages box.json install dir if provided. * @save.hint Save the installed package as a dependency in box.json (if it exists), defaults to true * @saveDev.hint Save the installed package as a dev dependency in box.json (if it exists) diff --git a/src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc b/src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc index 304bf51c..810baaa0 100644 --- a/src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc +++ b/src/cfml/system/modules_app/package-commands/interceptors/PackageScripts.cfc @@ -42,6 +42,7 @@ component { function preServerStart() { processScripts( 'preServerStart', shell.pwd(), interceptData ); } function onServerInstall() { processScripts( 'onServerInstall', interceptData.serverinfo.webroot, interceptData ); } + function onServerInitialInstall() { processScripts( 'onServerInitialInstall', interceptData.serverinfo.webroot, interceptData ); } function onServerStart() { processScripts( 'onServerStart', interceptData.serverinfo.webroot, interceptData ); } function onServerStop() { processScripts( 'onServerStop', interceptData.serverinfo.webroot, interceptData ); } function preServerForget() { processScripts( 'preServerForget', interceptData.serverinfo.webroot, interceptData ); } diff --git a/src/cfml/system/modules_app/server-commands/commands/server/open.cfc b/src/cfml/system/modules_app/server-commands/commands/server/open.cfc index 8fd6e1c4..5e3ada9a 100644 --- a/src/cfml/system/modules_app/server-commands/commands/server/open.cfc +++ b/src/cfml/system/modules_app/server-commands/commands/server/open.cfc @@ -75,6 +75,15 @@ component { if( serverDetails.serverIsNew ){ print.boldRedLine( "No servers found." ); } else { + var serverJSON = serverService.readServerJSON( serverDetails.defaultServerConfigFile ) + // If no explicit browser was provided to this command, but the server.json has one, use that. + if( !len( arguments.browser ) && len( serverJSON.preferredBrowser ?: '' ) ) { + arguments.browser = serverJSON.preferredBrowser; + } + if( !len( arguments.browser ) && len( serverInfo.preferredBrowser ) ) { + arguments.browser = serverInfo.preferredBrowser; + } + if ( arguments.webRoot ) { if ( fileSystemUtil.openNatively(serverInfo.appFileSystemPath) ) { print.line( "Web Root Opened." ); diff --git a/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc b/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc index 09cb818e..c73868d5 100644 --- a/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc +++ b/src/cfml/system/modules_app/server-commands/interceptors/ServerScripts.cfc @@ -18,6 +18,7 @@ component { function preServerStart() { processScripts( 'preServerStart', shell.pwd(), interceptData ); } function onServerInstall() { processScripts( 'onServerInstall', interceptData.serverinfo.webroot, interceptData ); } + function onServerInitialInstall() { processScripts( 'onServerInitialInstall', interceptData.serverinfo.webroot, interceptData ); } function onServerStart() { processScripts( 'onServerStart', interceptData.serverinfo.webroot, interceptData ); } function onServerStop() { processScripts( 'onServerStop', interceptData.serverinfo.webroot, interceptData ); } function preServerForget() { processScripts( 'preServerForget', interceptData.serverinfo.webroot, interceptData ); } diff --git a/src/cfml/system/modules_app/server-commands/interceptors/ServerSystemSettingExpansions.cfc b/src/cfml/system/modules_app/server-commands/interceptors/ServerSystemSettingExpansions.cfc index 3511b919..3a669ecb 100644 --- a/src/cfml/system/modules_app/server-commands/interceptors/ServerSystemSettingExpansions.cfc +++ b/src/cfml/system/modules_app/server-commands/interceptors/ServerSystemSettingExpansions.cfc @@ -45,11 +45,15 @@ component { } else if( interceptData.setting.lcase().startsWith( 'serverinfo.' ) ) { var settingName = interceptData.setting.replaceNoCase( 'serverinfo.', '', 'one' ); + var interceptData_serverInfo_name = systemSettings.getSystemSetting( 'interceptData.SERVERINFO.name', '' ); // Lookup by name if( listLen( settingName, '@' ) > 1 ) { var serverInfo = serverService.getServerInfoByName( listLast( settingName, '@' ) ); settingName = listFirst( settingName, '@' ); + // If we're running inside of a server-related package script, use that server + } else if( interceptData_serverInfo_name != '' ) { + var serverInfo = serverService.resolveServerDetails( { name=interceptData_serverInfo_name } ).serverInfo; // Lookup by current working directory } else { var serverInfo = serverService.getServerInfoByWebroot( shell.pwd() ); @@ -59,7 +63,7 @@ component { if( !serverInfo.count() && fileExists( serverJSONPath ) ) { var serverInfo = serverService.getServerInfoByServerConfigFile( serverJSONPath ); } - + interceptData.setting = JSONService.show( serverInfo, settingName, interceptData.defaultValue ); if( !isSimpleValue( interceptData.setting ) ) { diff --git a/src/cfml/system/modules_app/system-commands/commands/config/sync/diff.cfc b/src/cfml/system/modules_app/system-commands/commands/config/sync/diff.cfc new file mode 100644 index 00000000..7db7bc60 --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/config/sync/diff.cfc @@ -0,0 +1,111 @@ +/** + * Compare local config settings with remote settings for your user + * . + * {code:bash} + * config sync diff + * {code} + * . + **/ +component { + + property name="packageService" inject="packageService"; + property name="ConfigService" inject="ConfigService"; + property name="JSONService" inject="JSONService"; + property name="endpointService" inject="endpointService"; + property name="jsondiff" inject="jsondiff"; + + /** + * @endpointName Name of custom forgebox endpoint to use + * @endpointName.optionsUDF endpointNameComplete + * @overwrite Overwrite local settings entirely with remote settings + **/ + function run( string endpointName, boolean overwrite=false ) { + try { + + endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + + try { + var oEndpoint = endpointService.getEndpoint( endpointName ); + } catch( EndpointNotFound var e ) { + error( e.message, e.detail ?: '' ); + } + + var forgebox = oEndpoint.getForgebox(); + var APIToken = oEndpoint.getAPIToken(); + + if( !len( APIToken ) ) { + if( endpointName == 'forgebox' ) { + error( 'You don''t have a Forgebox API token set.', 'Use "forgebox login" to authenticate as a user.' ); + } else { + error( 'You don''t have a Forgebox API token set.', 'Use "endpoint login endpointName=#endpointName#" to authenticate as a user.' ); + } + } + + var modules = {}; + var directory = expandPath( '/commandbox' ); + // package check + if( packageService.isPackage( directory ) ) { + modules = packageService + .buildDependencyHierarchy( directory, 1 ) + .dependencies + .map( (s,p)=>p.version ); + } + + var userData = forgebox.whoami( APIToken ); + var remoteConfig = forgebox.getConfig( userData.username, APIToken ); + var configSettings = { + 'config' : duplicate( ConfigService.getconfigSettings( noOverrides=true ) ), + 'modules' : modules + }; + + + var diffDetails = jsondiff.diff(remoteConfig, configSettings ) + .reduce( ( diffDetails, item )=>{ + diffDetails[ item.type ].append( item ); + return diffDetails; + }, { add : [], remove : [], change : [] } ); + + if( diffDetails.remove.len() ) { + print.line().boldBlueLine( 'Remote-only settings:' ); + diffDetails.remove.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.old ) ) + } + + if( diffDetails.add.len() ) { + print.line().boldGreenLine( 'Local-only settings:' ); + diffDetails.add.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.new ) ) + } + + if( diffDetails.change.len() ) { + print.line().boldMagentaLine( 'Changed settings:' ); + diffDetails.change.each( ( item )=>{ + print.indentedLine( buildPath( item.path ) & ' = ' ) + .indentedIndentedBlue( 'Remote Value: ' ).line( item.old ) + .indentedIndentedGreen( 'Local Value: ' ).line( item.new ); + } ); + } + + if( !diffDetails.add.len() && !diffDetails.remove.len() && !diffDetails.change.len() ) { + print.boldGreenLine( "All config settings are identical between remote and local" ); + } + + } catch( forgebox var e ) { + // This can include "expected" errors such as "Email already in use" + error( e.message, e.detail ); + } + + } + + function buildPath( tokens ) { + return tokens.reduce( ( path, item )=>{ + if( isNumeric( item ) ) { + return path & "[#item#]"; + } + return path.listAppend( item, '.' ); + }, '' ); + } + + function endpointNameComplete() { + return getInstance( 'endpointService' ).forgeboxEndpointNameComplete(); + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/config/sync/pull.cfc b/src/cfml/system/modules_app/system-commands/commands/config/sync/pull.cfc new file mode 100644 index 00000000..9e3f54ee --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/config/sync/pull.cfc @@ -0,0 +1,134 @@ +/** + * Sync remote config settings with locaL settings for your user + * Settings will be merged together + * . + * {code:bash} + * config sync pull + * {code} + * . + * To completely replace local settings wtih remote settings, use the --overwrite flag + * . + * {code:bash} + * config sync pull --overwrite + * {code} + * + **/ +component { + + property name="packageService" inject="packageService"; + property name="ConfigService" inject="ConfigService"; + property name="JSONService" inject="JSONService"; + property name="endpointService" inject="endpointService"; + property name="jsondiff" inject="jsondiff"; + + /** + * @endpointName Name of custom forgebox endpoint to use + * @endpointName.optionsUDF endpointNameComplete + * @overwrite Overwrite local settings entirely with remote settings + **/ + function run( string endpointName, boolean overwrite=false ) { + try { + + endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + + try { + var oEndpoint = endpointService.getEndpoint( endpointName ); + } catch( EndpointNotFound var e ) { + error( e.message, e.detail ?: '' ); + } + + var forgebox = oEndpoint.getForgebox(); + var APIToken = oEndpoint.getAPIToken(); + + if( !len( APIToken ) ) { + if( endpointName == 'forgebox' ) { + error( 'You don''t have a Forgebox API token set.', 'Use "forgebox login" to authenticate as a user.' ); + } else { + error( 'You don''t have a Forgebox API token set.', 'Use "endpoint login endpointName=#endpointName#" to authenticate as a user.' ); + } + } + var modules = {}; + var directory = expandPath( '/commandbox' ); + // package check + if( packageService.isPackage( directory ) ) { + modules = packageService + .buildDependencyHierarchy( directory, 1 ) + .dependencies + .map( (s,p)=>p.version ); + } + var userData = forgebox.whoami( APIToken ); + var remoteConfig = forgebox.getConfig( userData.username, APIToken ); + var configSettings = { + 'config' : duplicate( ConfigService.getconfigSettings( noOverrides=true ) ), + 'modules' : modules + }; + + + + var diffDetails = jsondiff.diff(remoteConfig, configSettings ) + .reduce( ( diffDetails, item )=>{ + diffDetails[ item.type ].append( item ); + return diffDetails; + }, { add : [], remove : [], change : [] } ); + + if( diffDetails.remove.len() ) { + print.line().boldGreenLine( 'New incoming settings:' ); + diffDetails.remove.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.old ) ) + } + + if( diffDetails.add.len() && overwrite ) { + print.line().boldRedLine( 'Removed local settings:' ); + diffDetails.add.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.new ) ) + } + + if( diffDetails.change.len() ) { + print.line().boldYellowLine( 'Changed settings:' ); + diffDetails.change.each( ( item )=>{ + print.indentedLine( buildPath( item.path ) & ' = ' ) + .indentedIndentedRed( 'Old Value: ' ).line( item.new ) + .indentedIndentedGreen( 'New Value: ' ).line( item.old ); + } ); + } + + if( !overwrite ) { + remoteConfig = JSONService.mergeData( configSettings, remoteConfig ); + } + + configService.setConfigSettings( remoteConfig.config ); + print.line().greenLine( "ClI Settings imported" ).line(); + + diffDetails.remove + .filter( ( item )=>buildPath( item.path ).lCase().startsWith( 'modules.' ) ) + .each( ( item )=>command( 'install' ).params( buildPath( item.path ).listRest( '.' ) & '@' & item.old ).flags( 'system' ).run() ) + + diffDetails.change + .filter( ( item )=>buildPath( item.path ).lCase().startsWith( 'modules.' ) ) + .each( ( item )=>command( 'install' ).params( buildPath( item.path ).listRest( '.' ) & '@' & item.old ).flags( 'system' ).run() ) + + if( overwrite ) { + diffDetails.add + .filter( ( item )=>buildPath( item.path ).lCase().startsWith( 'modules.' ) ) + .each( ( item )=>command( 'uninstall' ).params( buildPath( item.path ).listRest( '.' ) ).flags( 'system' ).run() ) + } + + } catch( forgebox var e ) { + // This can include "expected" errors such as "Email already in use" + error( e.message, e.detail ); + } + + } + + function buildPath( tokens ) { + return tokens.reduce( ( path, item )=>{ + if( isNumeric( item ) ) { + return path & "[#item#]"; + } + return path.listAppend( item, '.' ); + }, '' ); + } + + function endpointNameComplete() { + return getInstance( 'endpointService' ).forgeboxEndpointNameComplete(); + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/config/sync/push.cfc b/src/cfml/system/modules_app/system-commands/commands/config/sync/push.cfc new file mode 100644 index 00000000..eb20b15c --- /dev/null +++ b/src/cfml/system/modules_app/system-commands/commands/config/sync/push.cfc @@ -0,0 +1,119 @@ +/** + * Sync local config settings with remote settings for your user + * Settings will be merged together + * . + * {code:bash} + * config sync push + * {code} + * . + * To completely replace remote settings wtih local settings, use the --overwrite flag + * . + * {code:bash} + * config sync push --overwrite + * {code} + * + **/ +component { + + property name="packageService" inject="packageService"; + property name="ConfigService" inject="ConfigService"; + property name="JSONService" inject="JSONService"; + property name="endpointService" inject="endpointService"; + property name="jsondiff" inject="jsondiff"; + + /** + * @endpointName Name of custom forgebox endpoint to use + * @endpointName.optionsUDF endpointNameComplete + * @overwrite Overwrite local settings entirely with remote settings + **/ + function run( string endpointName, boolean overwrite=false ) { + try { + + endpointName = endpointName ?: configService.getSetting( 'endpoints.defaultForgeBoxEndpoint', 'forgebox' ); + + try { + var oEndpoint = endpointService.getEndpoint( endpointName ); + } catch( EndpointNotFound var e ) { + error( e.message, e.detail ?: '' ); + } + + var forgebox = oEndpoint.getForgebox(); + var APIToken = oEndpoint.getAPIToken(); + + if( !len( APIToken ) ) { + if( endpointName == 'forgebox' ) { + error( 'You don''t have a Forgebox API token set.', 'Use "forgebox login" to authenticate as a user.' ); + } else { + error( 'You don''t have a Forgebox API token set.', 'Use "endpoint login endpointName=#endpointName#" to authenticate as a user.' ); + } + } + var modules = {}; + var directory = expandPath( '/commandbox' ); + // package check + if( packageService.isPackage( directory ) ) { + modules = packageService + .buildDependencyHierarchy( directory, 1 ) + .dependencies + .map( (s,p)=>p.version ); + } + var userData = forgebox.whoami( APIToken ); + var configSettings = { + 'config' : duplicate( ConfigService.getconfigSettings( noOverrides=true ) ), + 'modules' : modules + }; + var remoteConfig = forgebox.getConfig( userData.username, APIToken ); + + + + var diffDetails = jsondiff.diff(remoteConfig, configSettings ) + .reduce( ( diffDetails, item )=>{ + diffDetails[ item.type ].append( item ); + return diffDetails; + }, { add : [], remove : [], change : [] } ); + + if( diffDetails.remove.len() && overwrite ) { + print.line().boldRedLine( 'Removed remote settings:' ); + diffDetails.remove.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.old ) ) + } + + if( diffDetails.add.len() ) { + print.line().boldGreenLine( 'New remote settings:' ); + diffDetails.add.each( ( item )=>print.indented( buildPath( item.path ) & ' = ' ).line( item.new ) ) + } + + if( diffDetails.change.len() ) { + print.line().boldYellowLine( 'Changed settings:' ); + diffDetails.change.each( ( item )=>{ + print.indentedLine( buildPath( item.path ) & ' = ' ) + .indentedIndentedRed( 'Old Value: ' ).line( item.old ) + .indentedIndentedGreen( 'New Value: ' ).line( item.new ); + } ); + } + + if( !overwrite ) { + configSettings = JSONService.mergeData( remoteConfig, configSettings ); + } + print.line().greenLine( forgebox.setConfig( configSettings, userData.username, APIToken ) ); + + } catch( forgebox var e ) { + // This can include "expected" errors such as "Email already in use" + error( e.message, e.detail ); + } + + } + + function buildPath( tokens ) { + return tokens.reduce( ( path, item )=>{ + if( isNumeric( item ) ) { + return path & "[#item#]"; + } + return path.listAppend( item, '.' ); + }, '' ); + } + + + function endpointNameComplete() { + return getInstance( 'endpointService' ).forgeboxEndpointNameComplete(); + } + +} diff --git a/src/cfml/system/modules_app/system-commands/commands/more.cfc b/src/cfml/system/modules_app/system-commands/commands/more.cfc index 994bd190..0b8028a1 100644 --- a/src/cfml/system/modules_app/system-commands/commands/more.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/more.cfc @@ -9,12 +9,19 @@ component excludeFromHelp=true { /** - * @input.hint The piped input to be displayed. + * @input The piped input to be displayed or a file path to output. + * @input.optionsFileComplete true **/ function run( input='' ) { + + // If the input is a small-ish string with no line breaks, test it to see if it's a file path + if( len( input ) < 1000 && !find( input, chr(10) ) && !find( input, chr(13) ) && fileExists( resolvePath( input ) ) ) { + input = fileRead( resolvePath( input ) ); + } // Get terminal height var termHeight = shell.getTermHeight()-2; // Turn output into an array, breaking on carriage returns + input = input.replace( chr(13) & chr(10), chr(10), 'all' ); var content = listToArray( arguments.input, chr(13) & chr(10), true ); var key= ''; var i = 0; @@ -27,10 +34,10 @@ component excludeFromHelp=true { // pause for user input key = shell.waitForKey(); // If space, advance one line - if( key == 32 ) { + if( key == ' ' ) { StopAtLine++; - // If Ctrl+c, abort, ESC and q - } else if( key == 3 || key == 27 || key == 113 ){ + // If ESC or q + } else if( key == 'escape' || key == 'q' ){ print.redLine( 'Cancelled...' ); return; // Everything else is one page diff --git a/src/cfml/system/modules_app/system-commands/commands/sql.cfc b/src/cfml/system/modules_app/system-commands/commands/sql.cfc index f8a9b555..8b3776b7 100644 --- a/src/cfml/system/modules_app/system-commands/commands/sql.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/sql.cfc @@ -1,3 +1,4 @@ + /** * SQL query command for filtering table like data. This command will automatically * format any table like data into a query object that can be queried against @@ -73,11 +74,16 @@ component { string headerNames='' ) { - // Treat input as a potential file path + // Strip any incoming ANSI formatting arguments.data = print.unAnsi( arguments.data ); - //deserialize data if in a JSON format - if(isJSON(arguments.data)) arguments.data = deserializeJSON( arguments.data, false ); + // deserialize data if in a JSON format + // For very large JSON inputs, it's faster just to try than to call isJSON() which pre-parses the entire input again + try { + arguments.data = deserializeJSON( arguments.data, false ); + } catch( any e ) { + error( 'Input cannot be parsed as JSON. #e.message#', e.detail ); + } //format inputs into valid sql parts var dataQuery = isQuery( arguments.data ) ? arguments.data : convert.toQuery(arguments.data, arguments.headerNames); @@ -112,7 +118,7 @@ component { if( limit==0 ) { dataQueryResults = []; } else { - dataQueryResults = dataQueryResults.slice(offset, limit); + dataQueryResults = dataQueryResults.slice(offset, limit); } }; diff --git a/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc b/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc index 5e37b62b..59860018 100644 --- a/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc +++ b/src/cfml/system/modules_app/system-commands/commands/upgrade.cfc @@ -68,7 +68,14 @@ component { proxyPort="#ConfigService.getSetting( 'proxy.port', 80 )#" proxyUser="#ConfigService.getSetting( 'proxy.user', '' )#" proxyPassword="#ConfigService.getSetting( 'proxy.password', '' )#" - result="local.boxRepoResult"; + result="local.boxRepoResult" { + cfhttpparam(name="CLIID", type="url", value="#GetLuceeId().server.id#"); + cfhttpparam(name="CLIVersion", type="url", value="#shell.getVersion()#"); + cfhttpparam(name="os", type="url", value="#server.system.properties['os.name']#"); + cfhttpparam(name="jre", type="url", value="#server.java.version#"); + cfhttpparam(name="APIToken", type="url", value="#ConfigService.getSetting( 'endpoints.forgebox.APIToken', '' )#"); + } + http url="#loaderRepoURL#" @@ -78,7 +85,14 @@ component { proxyPort="#ConfigService.getSetting( 'proxy.port', 80 )#" proxyUser="#ConfigService.getSetting( 'proxy.user', '' )#" proxyPassword="#ConfigService.getSetting( 'proxy.password', '' )#" - result="local.loaderRepoResult"; + result="local.loaderRepoResult"{ + cfhttpparam(name="CLIID", type="url", value="#GetLuceeId().server.id#"); + cfhttpparam(name="CLIVersion", type="url", value="#shell.getVersion()#"); + cfhttpparam(name="os", type="url", value="#server.system.properties['os.name']#"); + cfhttpparam(name="jre", type="url", value="#server.java.version#"); + cfhttpparam(name="APIToken", type="url", value="#ConfigService.getSetting( 'endpoints.forgebox.APIToken', '' )#"); + } + // Do some error checking if( !local.boxRepoResult.statusCode contains "200" || !fileExists( '#temp#/box-repo.json' ) || diff --git a/src/cfml/system/services/ConfigService.cfc b/src/cfml/system/services/ConfigService.cfc index d8e9c2af..08e7be2c 100644 --- a/src/cfml/system/services/ConfigService.cfc +++ b/src/cfml/system/services/ConfigService.cfc @@ -25,6 +25,7 @@ component accessors="true" singleton { property name='ModuleService' inject='ModuleService'; property name='JSONService' inject='JSONService'; property name='ServerService' inject='provider:ServerService'; + property name='interceptorService' inject='interceptorService'; /** * Constructor @@ -244,6 +245,8 @@ component accessors="true" singleton { // Update ModuleService ModuleService.overrideAllConfigSettings(); + + interceptorService.announceInterception( 'onConfigSettingSave', { configFilePath=getConfigFilePath(), configSettings=getConfigSettings( noOverrides=true ) } ); } diff --git a/src/cfml/system/services/EndpointService.cfc b/src/cfml/system/services/EndpointService.cfc index 2fd847cb..88655db6 100644 --- a/src/cfml/system/services/EndpointService.cfc +++ b/src/cfml/system/services/EndpointService.cfc @@ -16,6 +16,7 @@ component accessors="true" singleton { property name="fileSystemUtil" inject="FileSystem"; property name="consoleLogger" inject="logbox:logger:console"; property name="configService" inject="configService"; + property name='interceptorService' inject='interceptorService'; // Properties @@ -30,7 +31,7 @@ component accessors="true" singleton { setEndpointRegistry( {} ); return this; } - + function onCLIStart() { buildEndpointRegistry(); registerCustomForgeboxEndpoints(); @@ -158,10 +159,10 @@ component accessors="true" singleton { if( endpointName == 'file' || endpointName == 'folder' ) { package = fileSystemUtil.resolvePath( package, arguments.currentWorkingDirectory ); if( endpointName == 'file' && !fileExists( package ) ) { - throw( "The file [ #package# ] does not exist.", 'endpointException' ); + throw( "The file [ #package# ] does not exist.", 'endpointException' ); } if( endpointName == 'folder' && !directoryExists( package ) ) { - throw( "The folder [ #package# ] does not exist.", 'endpointException' ); + throw( "The folder [ #package# ] does not exist.", 'endpointException' ); } theID = endpointName & ':' & package; } @@ -284,6 +285,8 @@ component accessors="true" singleton { // Store the APIToken endpoint.storeAPIToken( arguments.username, APIToken ); + interceptorService.announceInterception( 'onEndpointLogin', { endpointName=endpointName, username=username, endpoint=endpoint, APIToken=APIToken } ); + } /** diff --git a/src/cfml/system/services/InterceptorService.cfc b/src/cfml/system/services/InterceptorService.cfc index b1e539ab..dac7bc3d 100644 --- a/src/cfml/system/services/InterceptorService.cfc +++ b/src/cfml/system/services/InterceptorService.cfc @@ -33,13 +33,13 @@ component accessors=true singleton { // Module lifecycle 'preModuleLoad','postModuleLoad','preModuleUnLoad','postModuleUnload', // Server lifecycle - 'preServerStart','onServerStart','onServerInstall','onServerProcessLaunch','onServerStop','preServerForget','postServerForget', + 'preServerStart','onServerStart','onServerInstall','onServerInitialInstall','onServerProcessLaunch','onServerStop','preServerForget','postServerForget', // Error handling 'onException', // Package lifecycle 'preInstall','onInstall','postInstall','preUninstall','postUninstall','preVersion','postVersion','prePublish','postPublish','preUnpublish','postUnpublish','onRelease','preInstallAll','postInstallAll', // Misc - 'onSystemSettingExpansion' + 'onSystemSettingExpansion','onConfigSettingSave','onEndpointLogin' ] ); return this; diff --git a/src/cfml/system/services/ServerService.cfc b/src/cfml/system/services/ServerService.cfc index 4751bdcb..6e21308e 100644 --- a/src/cfml/system/services/ServerService.cfc +++ b/src/cfml/system/services/ServerService.cfc @@ -119,6 +119,7 @@ component accessors="true" singleton { return { 'name' : d.name ?: '', + 'preferredBrowser' : d.preferredBrowser ?: '', 'openBrowser' : d.openBrowser ?: true, 'openBrowserURL' : d.openBrowserURL ?: '', 'startTimeout' : 240, @@ -147,6 +148,7 @@ component accessors="true" singleton { 'host' : d.web.host ?: '127.0.0.1', 'directoryBrowsing' : d.web.directoryBrowsing ?: '', 'webroot' : d.web.webroot ?: '', + 'caseSensitivePaths' : d.web.caseSensitivePaths ?: '', // Duplicate so onServerStart interceptors don't actually change config settings via reference. 'aliases' : duplicate( d.web.aliases ?: {} ), // Duplicate so onServerStart interceptors don't actually change config settings via reference. @@ -224,7 +226,8 @@ component accessors="true" singleton { 'subjectDNs' : d.web.security.clientCert.subjectDNs ?: '', 'issuerDNs' : d.web.security.clientCert.issuerDNs ?: '' } - } + }, + 'mimeTypes' : duplicate( d.web.mimeTypes ?: {} ) }, 'app' : { 'logDir' : d.app.logDir ?: '', @@ -248,7 +251,11 @@ component accessors="true" singleton { // Duplicate so onServerStart interceptors don't actually change config settings via reference. 'XNIOOptions' : duplicate( d.runwar.XNIOOptions ?: {} ), // Duplicate so onServerStart interceptors don't actually change config settings via reference. - 'undertowOptions' : duplicate( d.runwar.undertowOptions ?: {} ) + 'undertowOptions' : duplicate( d.runwar.undertowOptions ?: {} ), + 'console' : { + 'appenderLayout' : d.runwar.console.appenderLayout ?: '', + 'appenderLayoutOptions' : duplicate( d.runwar.console.appenderLayoutOptions ?: {} ) + } }, 'ModCFML' : { 'enable' : d.ModCFML.enable ?: false, @@ -323,7 +330,7 @@ component accessors="true" singleton { var foundServer = getServerInfoByName( serverDetails.defaultName ); - if( structCount( foundServer ) && normalizeWebroot( foundServer.webroot ) != normalizeWebroot( serverDetails.defaultwebroot ) ) { + if( !isSingleServerMode() && structCount( foundServer ) && normalizeWebroot( foundServer.webroot ) != normalizeWebroot( serverDetails.defaultwebroot ) ) { throw( message='You''ve asked to start a server named [#serverDetails.defaultName#] with a webroot of [#serverDetails.defaultwebroot#], but a server of this name already exists with a different webroot of [#foundServer.webroot#]', detail='Server name and webroot must be unique. Please forget the old server first. Use "server list" to see all defined servers.', @@ -362,7 +369,7 @@ component accessors="true" singleton { // If the server is already running, make sure the user really wants to do this. if( isServerRunning( serverInfo ) && !(serverProps.force ?: false ) && !(serverProps.dryRun ?: false ) ) { - if( !shell.isTerminalInteractive() ) { + if( !shell.isTerminalInteractive() || isSingleServerMode() ) { throw( message="Cannot start server [#serverInfo.name#] because it is already running.", detail="Run [server info --verbose] to find out why CommandBox thinks this server is running.", type="commandException" ); } @@ -387,7 +394,7 @@ component accessors="true" singleton { } else if( action == 'openinbrowser' ) { job.addLog( "Opening...#serverInfo.openbrowserURL#" ); job.error( 'Aborting...' ); - shell.callCommand( 'browse #serverInfo.openbrowserURL#', false); + shell.callCommand( 'server open', false); return; } else if( action == 'newname' ) { job.clear(); @@ -599,6 +606,7 @@ component accessors="true" singleton { serverInfo.openbrowser = serverProps.openbrowser ?: serverJSON.openbrowser ?: defaults.openbrowser; serverInfo.openbrowserURL = serverProps.openbrowserURL ?: serverJSON.openbrowserURL ?: defaults.openbrowserURL; + serverInfo.preferredBrowser = serverJSON.preferredBrowser ?: defaults.preferredBrowser; // Trace assumes debug serverInfo.debug = serverInfo.trace || serverInfo.debug; @@ -732,19 +740,6 @@ component accessors="true" singleton { job[ 'add#( serverInfo.fileCacheEnable ? 'Success' : '' )#Log' ]( 'File Caching #( serverInfo.fileCacheEnable ? 'en' : 'dis' )#abled' ); job.complete( serverInfo.verbose ); - // Double check that the port in the user params or server.json isn't in use - if( !isPortAvailable( serverInfo.host, serverInfo.port ) ) { - job.addErrorLog( "" ); - var badPortlocation = 'config'; - if( serverProps.keyExists( 'port' ) ) { - badPortlocation = 'start params'; - } else if ( len( defaults.web.http.port ?: '' ) ) { - badPortlocation = 'server.json'; - } else { - badPortlocation = 'config server defaults'; - } - throw( message="You asked for port [#( serverProps.port ?: serverJSON.web.http.port ?: defaults.web.http.port ?: '?' )#] in your #badPortlocation# but it's already in use.", detail="Please choose another or use netstat to find out what process is using the port already.", type="commandException" ); - } serverInfo.stopsocket = serverProps.stopsocket ?: serverJSON.stopsocket ?: getRandomPort( serverInfo.host ); @@ -778,6 +773,21 @@ component accessors="true" singleton { serverInfo.AJPPort = serverProps.AJPPort ?: serverJSON.web.AJP.port ?: defaults.web.AJP.port; serverInfo.AJPSecret = serverJSON.web.AJP.secret ?: defaults.web.AJP.secret; + + // Double check that the port in the user params or server.json isn't in use + if( serverInfo.HTTPEnable && !isPortAvailable( serverInfo.host, serverInfo.port ) ) { + job.addErrorLog( "" ); + var badPortlocation = 'config'; + if( serverProps.keyExists( 'port' ) ) { + badPortlocation = 'start params'; + } else if ( len( defaults.web.http.port ?: '' ) ) { + badPortlocation = 'server.json'; + } else { + badPortlocation = 'config server defaults'; + } + throw( message="You asked for port [#serverInfo.port#] in your #badPortlocation# but it's already in use.", detail="Please choose another or use netstat to find out what process is using the port already.", type="commandException" ); + } + // relative certFile in server.json is resolved relative to the server.json if( isDefined( 'serverJSON.web.SSL.certFile' ) ) { serverJSON.web.SSL.certFile = fileSystemUtil.resolvePath( serverJSON.web.SSL.certFile, defaultServerConfigFileDirectory ); } // relative certFile in config setting server defaults is resolved relative to the web root @@ -886,6 +896,8 @@ component accessors="true" singleton { serverInfo.welcomeFiles = serverProps.welcomeFiles ?: serverJSON.web.welcomeFiles ?: defaults.web.welcomeFiles; serverInfo.maxRequests = serverJSON.web.maxRequests ?: defaults.web.maxRequests; + serverInfo.caseSensitivePaths= serverJSON.web.caseSensitivePaths ?: defaults.web.caseSensitivePaths; + serverInfo.trayEnable = serverProps.trayEnable ?: serverJSON.trayEnable ?: defaults.trayEnable; serverInfo.dockEnable = serverJSON.dockEnable ?: defaults.dockEnable; serverInfo.defaultBaseURL = serverInfo.SSLEnable ? 'https://#serverInfo.host#:#serverInfo.SSLPort#' : 'http://#serverInfo.host#:#serverInfo.port#'; @@ -961,6 +973,9 @@ component accessors="true" singleton { return fileSystemUtil.resolvePath( p, defaultServerConfigFileDirectory ); } ) ); + // Combine global and server-specific mime types + serverInfo.mimeTypes = defaults.web.mimeTypes.append( serverJSON.web.mimeTypes ?: {}, true ); + // Global errorPages are always added on top of server.json (but don't overwrite the full struct) // Aliases aren't accepted via command params serverInfo.errorPages = defaults.web.errorPages; @@ -1011,6 +1026,10 @@ component accessors="true" singleton { // Global defaults are always added on top of whatever is specified by the user or server.json serverInfo.runwarUndertowOptions = ( serverJSON.runwar.UndertowOptions ?: {} ).append( defaults.runwar.UndertowOptions, true ); + serverInfo.runwarAppenderLayout = serverJSON.runwar.console.appenderLayout ?: defaults.runwar.console.appenderLayout; + serverInfo.runwarAppenderLayoutOptions = serverJSON.runwar.console.appenderLayoutOptions ?: defaults.runwar.console.appenderLayoutOptions; + + // Server startup timeout serverInfo.startTimeout = serverProps.startTimeout ?: serverJSON.startTimeout ?: defaults.startTimeout; @@ -1254,6 +1273,9 @@ component accessors="true" singleton { setServerInfo( serverInfo ); // This interception point can be used for additional configuration of the engine before it actually starts. interceptorService.announceInterception( 'onServerInstall', { serverInfo=serverInfo, installDetails=installDetails, serverJSON=serverJSON, defaults=defaults, serverProps=serverProps, serverDetails=serverDetails } ); + if( installDetails.initialInstall ) { + interceptorService.announceInterception( 'onServerInitialInstall', { serverInfo=serverInfo, installDetails=installDetails, serverJSON=serverJSON, defaults=defaults, serverProps=serverProps, serverDetails=serverDetails } ); + } // If Lucee server, set the java agent if( serverInfo.cfengine contains "lucee" ) { @@ -1447,6 +1469,13 @@ component accessors="true" singleton { CLIAliases = CLIAliases.listAppend( thisAlias & '=' & serverInfo.aliases[ thisAlias ] ); } + // Turn struct of mimeTypes into a comma-delimited list + // "log;text/plain,foo;content/type" + var mimeTypesList = ''; + for( var thisMime in serverInfo.mimeTypes ) { + mimeTypesList = mimeTypesList.listAppend( thisMime & ';' & serverInfo.mimeTypes[ thisMime ] ); + } + // Turn struct of errorPages into a comma-delimited list. // --error-pages="404=/path/to/404.html,500=/path/to/500.html,1=/path/to/default.html" var errorPages = ''; @@ -1502,15 +1531,26 @@ component accessors="true" singleton { // Add java agent if( len( trim( javaAgent ) ) ) { argTokens.append( javaagent ); } - // TODOL Temp stopgap for Java regression that prevents Undertow from starting. - // https://issues.redhat.com/browse/UNDERTOW-2073 - // https://bugs.openjdk.java.net/browse/JDK-8285445 - if( !argTokens.filter( (a)=>a contains 'jdk.io.File.enableADS' ).len() ) { - argTokens.append( '-Djdk.io.File.enableADS=true' ); - } + // Collect recursive list of all jars in libDirs + jarArray = serverInfo.libDirs + .listToArray() + .reduce( function( jarArray, path ) { + if( fileExists( path ) && path.lcase().endsWith( '.jar' ) ) { + jarArray.append( path ); + } else if( directoryExists( path ) ) { + directoryList( path, true, 'array' ) + .filter( (p)=>p.lcase().endsWith( '.jar' ) ) + .each( function( p ) { + jarArray.append( p ); + } ); + } + return jarArray; + }, [] ); + jarArray.append( serverInfo.runwarJarPath ); args - .append( '-jar' ).append( serverInfo.runwarJarPath ) + .append( '-cp' ).append( jarArray.toList( server.system.properties[ 'path.separator' ] ) ) + .append( 'runwar.Start' ) .append( '--background=#background#' ) .append( '--host' ).append( serverInfo.host ) .append( '--stop-port' ).append( serverInfo.stopsocket ) @@ -1526,7 +1566,11 @@ component accessors="true" singleton { .append( '--cookie-httponly' ).append( serverInfo.sessionCookieHTTPOnly ) .append( '--pid-file').append( serverInfo.pidfile ); - if( ConfigService.settingExists( 'preferredBrowser' ) ) { + // If server.json has a default browser, use it + if( len( serverInfo.preferredBrowser ) ) { + args.append( '--preferred-browser' ).append( serverInfo.preferredBrowser ); + // Otherwise, use the global config setting, if it exists + } else if( ConfigService.settingExists( 'preferredBrowser' ) ) { args.append( '--preferred-browser' ).append( ConfigService.getSetting( 'preferredBrowser' ) ); } @@ -1556,6 +1600,13 @@ component accessors="true" singleton { args.append( '--undertow-options=' & serverInfo.runwarUndertowOptions.reduce( ( opts='', k, v ) => opts.listAppend( k & '=' & v ) ) ); } + if( len( serverInfo.runwarAppenderLayout ) ) { + args.append( '--console-layout' ).append( serverInfo.runwarAppenderLayout ); + } + if( serverInfo.runwarAppenderLayoutOptions.count() ) { + args.append( '--console-layout-options' ).append( serializeJSON( serverInfo.runwarAppenderLayoutOptions ) ); + } + if( serverInfo.debug ) { // Debug is getting turned on any time I include the --debug flag regardless of whether it's true or false. args.append( '--debug' ).append( serverInfo.debug ); @@ -1607,9 +1658,15 @@ component accessors="true" singleton { if( len( serverInfo.maxRequests ) ) { args.append( '--worker-threads' ).append( serverInfo.maxRequests ); } + if( len( serverInfo.caseSensitivePaths ) ) { + args.append( '--case-sensitive-web-server' ).append( serverInfo.caseSensitivePaths ); + } if( len( CLIAliases ) ) { args.append( '--dirs' ).append( CLIAliases ); } + if( len( mimeTypesList ) ) { + args.append( '--mime-types' ).append( mimeTypesList ); + } if( serverInfo.fileCacheEnable ) { args.append( '--cache-servlet-paths' ).append( true ); args.append( '--file-cache-total-size-mb' ).append( val( serverInfo.fileCacheTotalSizeMB ) ); @@ -1644,11 +1701,6 @@ component accessors="true" singleton { args.append( '--web-xml-override-force' ).append( serverInfo.webXMLOverrideForce ); } - if( len( serverInfo.libDirs ) ) { - // Have to get rid of empty list elements - args.append( '--lib-dirs' ).append( serverInfo.libDirs.listChangeDelims( ',', ',' ) ); - } - // Always send the enable flag for each protocol args .append( '--http-enable' ).append( serverInfo.HTTPEnable ) @@ -2122,6 +2174,9 @@ component accessors="true" singleton { function fixBinaryPath(command, fullPath){ if(!isNull(fullPath) or !isEmpty(fullPath)){ + if( fullPath contains ' ' ) { + fullPath = '"' & fullPath & '"'; + } if( command.left( 4 ) == 'box ' ){ command = command.replacenoCase( 'box ', fullPath & ' ', 'one' ); } else if( command.left( 8 ) == 'box.exe ' ){ @@ -2188,7 +2243,7 @@ component accessors="true" singleton { ) { // If CommandBox is in single server mode, just force the first (and only) server to be the one we find - if( ConfigService.getSetting( 'server.singleServerMode', false ) && getServers().count() ){ + if( isSingleServerMode() && getServers().count() ){ // CFConfig calls this method sometimes with a path to a JSON file and needs to get no server back if( serverProps.keyExists( 'name' ) && lcase( serverProps.name ).endsWith( '.json' ) ) { @@ -2201,16 +2256,6 @@ component accessors="true" singleton { serverIsNew : true }; } - - var serverInfo = getFirstServer(); - return { - defaultName : serverInfo.name, - defaultwebroot : serverInfo.webroot, - defaultServerConfigFile : serverInfo.serverConfigFile, - serverJSON : readServerJSON( serverInfo.serverConfigFile ), - serverInfo : serverInfo, - serverIsNew : false - }; } var job = wirebox.getInstance( 'interactiveJob' ); @@ -2279,6 +2324,20 @@ component accessors="true" singleton { serverConfigFile = serverProps.serverConfigFile ?: '' // Since this takes precedence, I only want to use it if it was actually specified ); + // If CommandBox is in single server mode, set our current values in so the "single server" always represents the last name and web root that was used. + if( isSingleServerMode() && getServers().count() ){ + if( len( defaultName ) ) { + serverInfo.name = defaultName; + } else { + serverInfo.name = replace( listLast( defaultwebroot, "\/" ), ':', ''); + } + serverInfo.webroot = defaultwebroot; + if( !isNull( serverProps.serverConfigFile ) ) { + serverInfo.serverConfigFile = serverProps.serverConfigFile; + } + setServerInfo( serverInfo ); + } + // If we found a server, set our name. if( len( serverInfo.name ?: '' ) ) { defaultName = serverInfo.name; @@ -2467,7 +2526,7 @@ component accessors="true" singleton { * @serverInfo The server information */ function getCustomServerFolder( required struct serverInfo ){ - if( configService.getSetting( 'server.singleServerMode', false ) ){ + if( isSingleServerMode() ){ return variables.customServerDirectory & 'serverHome'; } else { return variables.customServerDirectory & arguments.serverinfo.id & "-" & arguments.serverInfo.name; @@ -2621,7 +2680,7 @@ component accessors="true" singleton { function calculateServerID( webroot, name ) { - if( ConfigService.getSetting( 'server.singleServerMode', false ) ){ + if( isSingleServerMode() ){ return 'serverHome'; } var normalizedWebroot = normalizeWebroot( webroot ); @@ -2713,7 +2772,7 @@ component accessors="true" singleton { */ struct function getServerInfoByDiscovery( required directory="", required name="", serverConfigFile="" ){ - if( ConfigService.getSetting( 'server.singleServerMode', false ) && getServers().count() ){ + if( isSingleServerMode() && getServers().count() ){ return getFirstServer(); } @@ -2744,7 +2803,7 @@ component accessors="true" singleton { */ struct function getServerInfoByName( required name ){ - if( ConfigService.getSetting( 'server.singleServerMode', false ) && getServers().count() ){ + if( isSingleServerMode() && getServers().count() ){ return getFirstServer(); } @@ -2764,7 +2823,7 @@ component accessors="true" singleton { */ struct function getServerInfoByServerConfigFile( required serverConfigFile ){ - if( ConfigService.getSetting( 'server.singleServerMode', false ) && getServers().count() ){ + if( isSingleServerMode() && getServers().count() ){ return getFirstServer(); } @@ -2795,7 +2854,7 @@ component accessors="true" singleton { */ struct function getServerInfoByWebroot( required webroot ){ - if( ConfigService.getSetting( 'server.singleServerMode', false ) && getServers().count() ){ + if( isSingleServerMode() && getServers().count() ){ return getFirstServer(); } @@ -2940,10 +2999,12 @@ component accessors="true" singleton { 'dateLastStarted' : '', 'openBrowser' : true, 'openBrowserURL' : '', + 'preferredBrowser' : '', 'profile' : '', 'customServerFolder' : '', 'welcomeFiles' : '', 'maxRequests' : '', + 'caseSensitivePaths' : '', 'exitCode' : 0, 'rules' : [], 'rulesFile' : '', @@ -2958,7 +3019,8 @@ component accessors="true" singleton { 'HSTSEnable' : false, 'HSTSMaxAge' : 0, 'HSTSIncludeSubDomains' : false, - 'AJPSecret' : '' + 'AJPSecret' : '', + 'mimeTypes' : {} }; } @@ -3015,6 +3077,7 @@ component accessors="true" singleton { 'scripts' : { 'preServerStart' : '', 'onServerInstall' : '', + 'onServerInitialInstall' : '', 'onServerStart' : '', 'onServerStop' : '', 'preServerForget' : '', @@ -3177,6 +3240,10 @@ component accessors="true" singleton { } } + function isSingleServerMode() { + return configService.getSetting( 'server.singleServerMode', false ); + } + } diff --git a/src/cfml/system/util/CFMLExecutor.cfc b/src/cfml/system/util/CFMLExecutor.cfc index 643ef359..f5436065 100644 --- a/src/cfml/system/util/CFMLExecutor.cfc +++ b/src/cfml/system/util/CFMLExecutor.cfc @@ -30,11 +30,17 @@ component { savecontent variable="local.out"{ include "#arguments.template#"; } - return local.out; + if( len( local.out ) ) { + return local.out; + } + if( !isNull( variables.__result2 ) ) { + return variables.__result2; + } + return; } /** - * Execute a snipped of code in the context of a directory + * Execute a snippet of code in the context of a directory * @code.hint CFML code to run * @script.hint is the CFML code script or tags * @directory.hint Absolute path to a directory context to run in @@ -47,7 +53,7 @@ component { var tmpFileAbsolute = arguments.directory & "/" & tmpFile; // generate cfml command to write to file - var CFMLFileContents = ( arguments.script ? "" & arguments.code & "" : arguments.code ); + var CFMLFileContents = ( arguments.script ? "variables.__result2 = " & arguments.code & "" : arguments.code ); // write out our cfml command fileWrite( tmpFileAbsolute, CFMLFileContents ); @@ -55,7 +61,14 @@ component { try { return runFile( tmpFileAbsolute, arguments.vars ); } catch( any e ){ - rethrow; + + // generate cfml command to write to file + local.CFMLFileContents = ( arguments.script ? "" & arguments.code & "" : arguments.code ); + + // write out our cfml command + fileWrite( tmpFileAbsolute, CFMLFileContents ); + + return runFile( tmpFileAbsolute, arguments.vars ); } finally { // cleanup if( fileExists( tmpFileAbsolute ) ){ @@ -98,4 +111,25 @@ component { } ); } + /** + * This method mimics a Java/Groovy assert() function, where it evaluates the target to a boolean value or an executable closure and it must be true + * to pass and return a true to you, or throw an `AssertException` + * + * @target The tareget to evaluate for being true, it can also be a closure that will be evaluated at runtime + * @message The message to send in the exception + * + * @throws AssertException if the target is a false or null value + * @return True, if the target is a non-null value. If false, then it will throw the `AssertError` exception + */ + boolean function assert( target, message="" ){ + // param against nulls + arguments.target = arguments.target ?: false; + // evaluate it + var results = isClosure( arguments.target ) || isCustomFunction( arguments.target ) ? arguments.target( variables ) : arguments.target; + // deal it : callstack two is from where the `assert` was called. + + + return results ? true : throw( message="Assertion failed", detail=arguments.message, type="commandException" ); + } + } diff --git a/src/cfml/system/util/ForgeBox.cfc b/src/cfml/system/util/ForgeBox.cfc index 7c998cf5..bcb30db8 100644 --- a/src/cfml/system/util/ForgeBox.cfc +++ b/src/cfml/system/util/ForgeBox.cfc @@ -223,6 +223,50 @@ or just add DEBUG to the root logger return results.response.data; } + /** + * set user config + */ + function setConfig( required struct config, required string username, required string APIToken ) { + + var results = makeRequest( + resource="users/#username#/clisettings", + method='post', + formFields={ + 'CLISettings' : serializeJSON( config ) + }, + headers = { + 'x-api-token' : arguments.APIToken, + "Content-Type" = "application/x-www-form-urlencoded" + } ); + + // error + if( results.response.error ){ + throw( arrayToList( results.response.messages ), 'forgebox' ); + } + + return arrayToList( results.response.messages ); + } + + /** + * set user config + */ + function getConfig( required string username, required string APIToken ) { + + var results = makeRequest( + resource="users/#username#/clisettings", + method='get', + headers = { + 'x-api-token' : arguments.APIToken + } ); + + // error + if( results.response.error ){ + throw( arrayToList( results.response.messages ), 'forgebox' ); + } + + return deserializeJSON( results.response.data ); + } + /** * Authenticates a user in ForgeBox */ @@ -363,13 +407,14 @@ or just add DEBUG to the root logger string typeSlug = '', string APIToken='' ) { - var thisResource = "slugs/#arguments.searchTerm#"; + var thisResource = "slugs"; var results = makeRequest( resource=thisResource, method='get', parameters={ - typeSlug : arguments.typeSlug + typeSlug : arguments.typeSlug, + searchTerm : arguments.searchTerm }, headers = { 'x-api-token' : arguments.APIToken @@ -429,9 +474,9 @@ or just add DEBUG to the root logger if( configService.getSetting( 'offlineMode', false ) ) { - throw( 'Can''t access #getEndpointName()# resource [#resource#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'forgebox' ); + throw( 'Can''t access #getEndpointName()# resource [#resource#], CommandBox is in offline mode. Go online with [config set offlineMode=false].', 'forgebox' ); } - + var results = {error=false,response={},message="",responseheader={},rawResponse=""}; var HTTPResults = ""; var param = ""; @@ -480,7 +525,6 @@ or just add DEBUG to the root logger } // structDelete( arguments.headers, "Content-Type" ); - diff --git a/src/java/cliloader/LoaderCLIMain.java b/src/java/cliloader/LoaderCLIMain.java index ab8824c0..44ec862f 100644 --- a/src/java/cliloader/LoaderCLIMain.java +++ b/src/java/cliloader/LoaderCLIMain.java @@ -302,7 +302,8 @@ && new File( cliArguments.get( 0 ) ).isFile() ) { System.setProperty( "lucee.base.dir", getLuceeCLIConfigServerDir().getAbsolutePath() ); // A couple tweaks to make Felix faster System.setProperty( "felix.cache.locking", "false" ); - System.setProperty( "felix.storage.clean", "none" ); + System.setProperty( "org.osgi.framework.storage.clean", "none" ); + // System.setProperty( "felix.log.level", System.getProperty( "felix.log.level", System.getenv().getOrDefault("FELIX_LOG_LEVEL", "0" ) ) ); // Load up JSR-223! ScriptEngineManager engineManager = new ScriptEngineManager( cl );