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 );