From 7d9aeb21f551350eda6e5dd980dbb7fab46c2aaa Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Tue, 23 Apr 2024 19:27:05 -0400 Subject: [PATCH 01/17] First commit for JSON substitutions (WIP) --- src/qz/ws/substitutions/RestrictedKey.java | 20 ++ src/qz/ws/substitutions/RootKey.java | 63 +++++ src/qz/ws/substitutions/Substitutions.java | 229 ++++++++++++++++++ .../ws/substitutions/SubstitutionsTests.java | 152 ++++++++++++ 4 files changed, 464 insertions(+) create mode 100644 src/qz/ws/substitutions/RestrictedKey.java create mode 100644 src/qz/ws/substitutions/RootKey.java create mode 100644 src/qz/ws/substitutions/Substitutions.java create mode 100644 test/qz/ws/substitutions/SubstitutionsTests.java diff --git a/src/qz/ws/substitutions/RestrictedKey.java b/src/qz/ws/substitutions/RestrictedKey.java new file mode 100644 index 000000000..a25a7db98 --- /dev/null +++ b/src/qz/ws/substitutions/RestrictedKey.java @@ -0,0 +1,20 @@ +package qz.ws.substitutions; + +public enum RestrictedKey { + COPIES("copies", RootKey.OPTIONS), + DATA("data", RootKey.DATA); + private String subkey; + private RootKey parent; + RestrictedKey(String subkey, RootKey parent) { + this.subkey = subkey; + this.parent = parent; + } + + public RootKey getParent() { + return parent; + } + + public String getSubkey() { + return subkey; + } +} \ No newline at end of file diff --git a/src/qz/ws/substitutions/RootKey.java b/src/qz/ws/substitutions/RootKey.java new file mode 100644 index 000000000..cfa367648 --- /dev/null +++ b/src/qz/ws/substitutions/RootKey.java @@ -0,0 +1,63 @@ +package qz.ws.substitutions; + +import java.util.ArrayList; + +public enum RootKey { + OPTIONS(true, "options", "config"), + PRINTER(true, "printer"), + DATA(true, "data"); + + private String key; + private String[] alts; + private boolean replaceAllowed; + private ArrayList restrictedKeys; + + RootKey(boolean replaceAllowed, String key, String... alts) { + this.replaceAllowed = replaceAllowed; + this.key = key; + this.alts = alts; + this.restrictedKeys = getRestrictedSubkeys(); + } + + public boolean isReplaceAllowed() { + return replaceAllowed; + } + + public boolean isSubkeyRestricted(String subkey) { + for(RestrictedKey key : restrictedKeys) { + if (key.getSubkey().equals(subkey)) { + return true; + } + } + return false; + } + + public static RootKey parse(Object o) { + for(RootKey root : values()) { + if (root.key.equals(o)) { + return root; + } + } + return null; + } + + public String getKey() { + return key; + } + + public String[] getAlts() { + return alts; + } + + public ArrayList getRestrictedSubkeys() { + if (restrictedKeys == null) { + restrictedKeys = new ArrayList(); + for(RestrictedKey restricted : RestrictedKey.values()) { + if (restricted.getParent() == this) { + restrictedKeys.add(restricted); + } + } + } + return restrictedKeys; + } +} \ No newline at end of file diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java new file mode 100644 index 000000000..a1d3b4e2c --- /dev/null +++ b/src/qz/ws/substitutions/Substitutions.java @@ -0,0 +1,229 @@ +package qz.ws.substitutions; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +public class Substitutions { + protected static final Logger log = LogManager.getLogger(Substitutions.class); + private static boolean restrictSubstitutions = true; + private ArrayList matches; + private ArrayList replaces; + + private static class SubstitutionException extends JSONException { + public SubstitutionException(String message) { + super(message); + } + } + + public Substitutions(String serialized) throws JSONException { + matches = new ArrayList<>(); + replaces = new ArrayList<>(); + + JSONArray instructions = new JSONArray(serialized); + System.out.println(serialized); + for(int i = 0; i < instructions.length(); i++) { + JSONObject step = instructions.optJSONObject(i); + if(step != null) { + JSONObject replace = step.optJSONObject("use"); + if(replace != null) { + sanitize(replace); + this.replaces.add(replace); + } + + JSONObject match = step.optJSONObject("for"); + if(match != null) { + sanitize(match); + this.matches.add(match); + } + } + } + if(matches.size() != replaces.size()) { + throw new SubstitutionException("Mismatched instructions; Each \"use\" must have a matching \"for\"."); + } + } + + public void replace(JSONObject base) throws JSONException { + for(int i = 0; i < matches.size(); i++) { + if (find(base, matches.get(i))) { + System.out.println(" [YES MATCH]"); + replace(base, replaces.get(i)); + } else { + System.out.println(" [NO MATCH]"); + } + } + } + + public static boolean isPrimitive(Object o) { + if(o instanceof JSONObject || o instanceof JSONArray) { + return false; + } + return true; + } + + public static void replace(JSONObject base, JSONObject replace) throws JSONException { + JSONObject baseParams = base.optJSONObject("params"); + JSONObject replaceParams = replace.optJSONObject("params"); + if(baseParams == null) { + // skip, invalid base format for replacement + return; + } + if(replaceParams == null) { + throw new SubstitutionException("Replacement JSON is missing \"params\": and is malformed"); + } + + // Second pass of sanitization before we replace + for(Iterator it = replaceParams.keys(); it.hasNext();) { + RootKey root = RootKey.parse(it.next()); + if(root != null && root.isReplaceAllowed()) { + // Good, let's make sure there are no exceptions + if(restrictSubstitutions) { + switch(root) { + // Special handling for arrays + case DATA: + JSONArray data = (JSONArray)replaceParams.get(root.getKey()); + ArrayList toRemove = new ArrayList(); + for(int i = 0; i < data.length(); i++) { + JSONObject jsonObject; + if ((jsonObject = data.optJSONObject(i)) != null) { + for(RestrictedKey restricted : root.getRestrictedSubkeys()) { + if (jsonObject.has(restricted.getSubkey())) { + log.warn("Use of {}: [{}:] is restricted, removing", root.getKey(), restricted.getSubkey()); + toRemove.add(jsonObject); + } + } + } + } + for(Object o : toRemove) { + data.remove(o); + } + break; + default: + for(RestrictedKey restricted : root.getRestrictedSubkeys()) { + if (replaceParams.has(restricted.getSubkey())) { + log.warn("Use of {}: [{}:] is restricted, removing", root.getKey(), restricted.getSubkey()); + replaceParams.remove(restricted.getSubkey()); + } + } + } + } + } + } + find(base, replace, true); + } + + public static void sanitize(JSONObject match) throws JSONException { + // "options" ~= "config" + System.out.println("BEFORE: " + match); + Object cache; + + + for(RootKey key : RootKey.values()) { + // Sanitize alts/aliases + for(String alt : key.getAlts()) { + if ((cache = match.optJSONObject(alt)) != null) { + match.put(key.getKey(), cache); + match.remove(alt); + break; + } + } + + // Special handling for nesting of "printer", "options", "data" within "params" + if((cache = match.opt(key.getKey())) != null) { + JSONObject nested = new JSONObject(); + switch(key) { + case PRINTER: + JSONObject name = new JSONObject(); + name.put("name", cache); + nested.put(key.getKey(), name); + break; + default: + nested.put(key.getKey(), cache); + } + + match.put("params", nested); + match.remove(key.getKey()); + } + } + + // Special handling for "data" being provided as an object instead of an array + if((cache = match.opt("params")) != null) { + if (cache instanceof JSONObject) { + JSONObject params = (JSONObject)cache; + if((cache = params.opt("data")) != null) { + if (cache instanceof JSONArray) { + // correct + } else { + JSONArray wrapped = new JSONArray(); + wrapped.put(cache); + params.put("data", wrapped); + } + } + } + } + + System.out.println("AFTER: " + match); + } + + private static boolean find(Object base, Object match) throws JSONException { + return find(base, match, false); + } + private static boolean find(Object base, Object match, boolean replace) throws JSONException { + if(base instanceof JSONObject) { + if(match instanceof JSONObject) { + JSONObject jsonMatch = (JSONObject)match; + JSONObject jsonBase = (JSONObject)base; + for(Iterator it = jsonMatch.keys(); it.hasNext(); ) { + Object next = it.next(); + Object newMatch = jsonMatch.get(next.toString()); + + // Check if the key exists, recurse if needed + if(jsonBase.has(next.toString())) { + Object newBase = jsonBase.get(next.toString()); + + if(replace && isPrimitive(newMatch)) { + // Overwrite value, don't recurse + jsonBase.put(next.toString(), newMatch); + continue; + } else if(find(newBase, newMatch, replace)) { + continue; + } + } else if(replace) { + // Key doesn't exist, so we'll merge it in + jsonBase.put(next.toString(), newMatch); + } + return false; // wasn't found + } + return true; // assume found + } else { + return false; // mismatched types + } + } else if (base instanceof JSONArray) { + if(match instanceof JSONArray) { + JSONArray matchArray = (JSONArray)match; + JSONArray baseArray = (JSONArray)base; + match: + for(int i = 0; i < matchArray.length(); i++) { + Object newMatch = matchArray.get(i); + for(int j = 0; j < baseArray.length(); j++) { + Object newBase = baseArray.get(j); + if(find(newBase, newMatch, replace)) { + continue match; + } + } + return false; + } + return true; // assume found + } else { + return false; + } + } else { + // Treat as primitives + return match.equals(base); + } + } +} diff --git a/test/qz/ws/substitutions/SubstitutionsTests.java b/test/qz/ws/substitutions/SubstitutionsTests.java new file mode 100644 index 000000000..5d30ca508 --- /dev/null +++ b/test/qz/ws/substitutions/SubstitutionsTests.java @@ -0,0 +1,152 @@ +package qz.ws.substitutions; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; + +public class SubstitutionsTests { + public static String[] JSON_TEST_MATCH = new String[4]; + public static String[] JSON_TEST_REPLACE = new String[4]; + static { + JSON_TEST_MATCH[0] = "{\n" + + " \"config\": {\n" + + " \"size\": {\n" + + " \"width\": \"4\",\n" + + " \"height\": \"6\"\n" + + " }\n" + + " }\n" + + "}"; + JSON_TEST_MATCH[1] = "{\n" + + " \"printer\": \"PDFwriter\"\n" + + "}"; + JSON_TEST_MATCH[2] = "{\n" + + " \"data\": {\n" + + " \"options\": {\n" + + " \"pageWidth\": \"8.5\",\n" + + " \"pageHeight\": \"11\"\n" + + " }\n" + + " }\n" + + "}"; + + JSON_TEST_MATCH[3] = "{\n" + + " \"config\": {\n" + + " \"copies\": 1" + + " }" + + "}"; + + JSON_TEST_REPLACE[0] = "{\n" + + " \"config\": {\n" + + " \"size\": {\n" + + " \"width\": \"100\",\n" + + " \"height\": \"150\"\n" + + " },\n" + + " \"units\": \"mm\"\n" + + " }\n" + + "}"; + JSON_TEST_REPLACE[1] = "{\n" + + " \"printer\": \"XPS Document Writer\"\n" + + "}"; + JSON_TEST_REPLACE[2] = "{\n" + + " \"data\": {\n" + + " \"options\": {\n" + + " \"pageWidth\": \"8.5\",\n" + + " \"pageHeight\": \"14\"\n" + + " }\n" + + " }\n" + + "}"; + + JSON_TEST_REPLACE[3] = "{\n" + + " \"config\": {\n" + + " \"copies\": 3" + + " }" + + "}"; + } + + public static final String JSON_TEST_BASE = "{\n" + + " \"call\": \"print\",\n" + + " \"params\": {\n" + + " \"printer\": {\n" + + " \"name\": \"PDFwriter\"\n" + + " },\n" + + " \"options\": {\n" + + " \"bounds\": null,\n" + + " \"colorType\": \"color\",\n" + + " \"copies\": 1,\n" + + " \"density\": 0,\n" + + " \"duplex\": false,\n" + + " \"fallbackDensity\": null,\n" + + " \"interpolation\": \"bicubic\",\n" + + " \"jobName\": null,\n" + + " \"legacy\": false,\n" + + " \"margins\": 0,\n" + + " \"orientation\": null,\n" + + " \"paperThickness\": null,\n" + + " \"printerTray\": null,\n" + + " \"rasterize\": false,\n" + + " \"rotation\": 0,\n" + + " \"scaleContent\": true,\n" + + " \"size\": {\n" + + " \"width\": \"4\",\n" + + " \"height\": \"6\"\n" + + " },\n" + + " \"units\": \"in\",\n" + + " \"forceRaw\": false,\n" + + " \"encoding\": null,\n" + + " \"spool\": {}\n" + + " },\n" + + " \"data\": [\n" + + " {\n" + + " \"type\": \"pixel\",\n" + + " \"format\": \"pdf\",\n" + + " \"flavor\": \"file\",\n" + + " \"data\": \"https://demo.qz.io/assets/pdf_sample.pdf\",\n" + + " \"options\": {\n" + + " \"pageWidth\": \"8.5\",\n" + + " \"pageHeight\": \"11\",\n" + + " \"pageRanges\": \"\",\n" + + " \"ignoreTransparency\": false,\n" + + " \"altFontRendering\": false\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"signature\": \"\",\n" + + " \"timestamp\": 1713895560783,\n" + + " \"uid\": \"64t63d\",\n" + + " \"position\": {\n" + + " \"x\": 720,\n" + + " \"y\": 462.5\n" + + " },\n" + + " \"signAlgorithm\": \"SHA512\"\n" + + "}"; + + public static void main(String ... args) throws JSONException { + JSONArray instructions = new JSONArray(); + + /** TODO: Move the tests to a flat file **/ + JSONObject step1 = new JSONObject(); + step1.put("use", new JSONObject(JSON_TEST_REPLACE[0])); + step1.put("for", new JSONObject(JSON_TEST_MATCH[0])); + + JSONObject step2 = new JSONObject(); + step2.put("use", new JSONObject(JSON_TEST_REPLACE[1])); + step2.put("for", new JSONObject(JSON_TEST_MATCH[1])); + + JSONObject step3 = new JSONObject(); + step3.put("use", new JSONObject(JSON_TEST_REPLACE[2])); + step3.put("for", new JSONObject(JSON_TEST_MATCH[2])); + + // TODO: Why doesn't this test fail on "options" with subkey of "copies" + JSONObject step4 = new JSONObject(); + step4.put("use", new JSONObject(JSON_TEST_REPLACE[3])); + step4.put("for", new JSONObject(JSON_TEST_MATCH[3])); + + instructions.put(step1).put(step2).put(step3).put(step4); + Substitutions substitutions = new Substitutions(instructions.toString()); + + JSONObject base = new JSONObject(JSON_TEST_BASE); + substitutions.replace(base); + + System.out.println(base); + } +} From 4a4d7a036acaa15af081d281d86c1230a3174b5b Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Wed, 24 Apr 2024 02:16:42 -0400 Subject: [PATCH 02/17] Various cleanup --- src/qz/ws/substitutions/RestrictedKey.java | 20 --- src/qz/ws/substitutions/RootKey.java | 63 -------- .../substitutions/SubstitutionException.java | 9 ++ src/qz/ws/substitutions/Substitutions.java | 120 +++++++++----- src/qz/ws/substitutions/Type.java | 41 +++++ .../ws/substitutions/SubstitutionsTests.java | 151 ++---------------- .../substitutions/resources/printRequest.json | 57 +++++++ .../resources/substitutions.json | 71 ++++++++ 8 files changed, 263 insertions(+), 269 deletions(-) delete mode 100644 src/qz/ws/substitutions/RestrictedKey.java delete mode 100644 src/qz/ws/substitutions/RootKey.java create mode 100644 src/qz/ws/substitutions/SubstitutionException.java create mode 100644 src/qz/ws/substitutions/Type.java create mode 100644 test/qz/ws/substitutions/resources/printRequest.json create mode 100644 test/qz/ws/substitutions/resources/substitutions.json diff --git a/src/qz/ws/substitutions/RestrictedKey.java b/src/qz/ws/substitutions/RestrictedKey.java deleted file mode 100644 index a25a7db98..000000000 --- a/src/qz/ws/substitutions/RestrictedKey.java +++ /dev/null @@ -1,20 +0,0 @@ -package qz.ws.substitutions; - -public enum RestrictedKey { - COPIES("copies", RootKey.OPTIONS), - DATA("data", RootKey.DATA); - private String subkey; - private RootKey parent; - RestrictedKey(String subkey, RootKey parent) { - this.subkey = subkey; - this.parent = parent; - } - - public RootKey getParent() { - return parent; - } - - public String getSubkey() { - return subkey; - } -} \ No newline at end of file diff --git a/src/qz/ws/substitutions/RootKey.java b/src/qz/ws/substitutions/RootKey.java deleted file mode 100644 index cfa367648..000000000 --- a/src/qz/ws/substitutions/RootKey.java +++ /dev/null @@ -1,63 +0,0 @@ -package qz.ws.substitutions; - -import java.util.ArrayList; - -public enum RootKey { - OPTIONS(true, "options", "config"), - PRINTER(true, "printer"), - DATA(true, "data"); - - private String key; - private String[] alts; - private boolean replaceAllowed; - private ArrayList restrictedKeys; - - RootKey(boolean replaceAllowed, String key, String... alts) { - this.replaceAllowed = replaceAllowed; - this.key = key; - this.alts = alts; - this.restrictedKeys = getRestrictedSubkeys(); - } - - public boolean isReplaceAllowed() { - return replaceAllowed; - } - - public boolean isSubkeyRestricted(String subkey) { - for(RestrictedKey key : restrictedKeys) { - if (key.getSubkey().equals(subkey)) { - return true; - } - } - return false; - } - - public static RootKey parse(Object o) { - for(RootKey root : values()) { - if (root.key.equals(o)) { - return root; - } - } - return null; - } - - public String getKey() { - return key; - } - - public String[] getAlts() { - return alts; - } - - public ArrayList getRestrictedSubkeys() { - if (restrictedKeys == null) { - restrictedKeys = new ArrayList(); - for(RestrictedKey restricted : RestrictedKey.values()) { - if (restricted.getParent() == this) { - restrictedKeys.add(restricted); - } - } - } - return restrictedKeys; - } -} \ No newline at end of file diff --git a/src/qz/ws/substitutions/SubstitutionException.java b/src/qz/ws/substitutions/SubstitutionException.java new file mode 100644 index 000000000..e2fa3fe85 --- /dev/null +++ b/src/qz/ws/substitutions/SubstitutionException.java @@ -0,0 +1,9 @@ +package qz.ws.substitutions; + +import org.codehaus.jettison.json.JSONException; + +public class SubstitutionException extends JSONException { + public SubstitutionException(String message) { + super(message); + } +} diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index a1d3b4e2c..e9783f7f7 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -1,23 +1,34 @@ package qz.ws.substitutions; +import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; + public class Substitutions { protected static final Logger log = LogManager.getLogger(Substitutions.class); + + // Subkeys that are restricted for writing private static boolean restrictSubstitutions = true; - private ArrayList matches; - private ArrayList replaces; + private static HashMap restricted = new HashMap<>(); + static { + restricted.put("copies", Type.OPTIONS); + restricted.put("data", Type.DATA); + } + private ArrayList matches, replaces; - private static class SubstitutionException extends JSONException { - public SubstitutionException(String message) { - super(message); - } + public Substitutions(InputStream in) throws IOException, JSONException { + this(IOUtils.toString(in, StandardCharsets.UTF_8)); } public Substitutions(String serialized) throws JSONException { @@ -25,7 +36,6 @@ public Substitutions(String serialized) throws JSONException { replaces = new ArrayList<>(); JSONArray instructions = new JSONArray(serialized); - System.out.println(serialized); for(int i = 0; i < instructions.length(); i++) { JSONObject step = instructions.optJSONObject(i); if(step != null) { @@ -47,15 +57,20 @@ public Substitutions(String serialized) throws JSONException { } } - public void replace(JSONObject base) throws JSONException { + public JSONObject replace(InputStream in) throws IOException, JSONException { + return replace(new JSONObject(IOUtils.toString(in, StandardCharsets.UTF_8))); + } + + public JSONObject replace(JSONObject base) throws JSONException { for(int i = 0; i < matches.size(); i++) { if (find(base, matches.get(i))) { - System.out.println(" [YES MATCH]"); + log.debug("Matched JSON substitution rule: for: {}, use: {}", matches.get(i), replaces.get(i)); replace(base, replaces.get(i)); } else { - System.out.println(" [NO MATCH]"); + log.debug("Unable to match substitution rule: for: {}, use: {}", matches.get(i), replaces.get(i)); } } + return base; } public static boolean isPrimitive(Object o) { @@ -66,49 +81,30 @@ public static boolean isPrimitive(Object o) { } public static void replace(JSONObject base, JSONObject replace) throws JSONException { - JSONObject baseParams = base.optJSONObject("params"); - JSONObject replaceParams = replace.optJSONObject("params"); - if(baseParams == null) { + JSONObject jsonBase = base.optJSONObject("params"); + JSONObject jsonReplace = replace.optJSONObject("params"); + if(jsonBase == null) { // skip, invalid base format for replacement return; } - if(replaceParams == null) { + if(jsonReplace == null) { throw new SubstitutionException("Replacement JSON is missing \"params\": and is malformed"); } // Second pass of sanitization before we replace - for(Iterator it = replaceParams.keys(); it.hasNext();) { - RootKey root = RootKey.parse(it.next()); - if(root != null && root.isReplaceAllowed()) { + for(Iterator it = jsonReplace.keys(); it.hasNext();) { + Type type = Type.parse(it.next()); + if(type != null && !type.isReadOnly()) { // Good, let's make sure there are no exceptions if(restrictSubstitutions) { - switch(root) { + switch(type) { // Special handling for arrays case DATA: - JSONArray data = (JSONArray)replaceParams.get(root.getKey()); - ArrayList toRemove = new ArrayList(); - for(int i = 0; i < data.length(); i++) { - JSONObject jsonObject; - if ((jsonObject = data.optJSONObject(i)) != null) { - for(RestrictedKey restricted : root.getRestrictedSubkeys()) { - if (jsonObject.has(restricted.getSubkey())) { - log.warn("Use of {}: [{}:] is restricted, removing", root.getKey(), restricted.getSubkey()); - toRemove.add(jsonObject); - } - } - } - } - for(Object o : toRemove) { - data.remove(o); - } + JSONArray jsonArray = jsonReplace.optJSONArray(type.getKey()); + removeRestrictedSubkeys(jsonArray, type); break; default: - for(RestrictedKey restricted : root.getRestrictedSubkeys()) { - if (replaceParams.has(restricted.getSubkey())) { - log.warn("Use of {}: [{}:] is restricted, removing", root.getKey(), restricted.getSubkey()); - replaceParams.remove(restricted.getSubkey()); - } - } + removeRestrictedSubkeys(jsonReplace, type); } } } @@ -116,13 +112,47 @@ public static void replace(JSONObject base, JSONObject replace) throws JSONExcep find(base, replace, true); } + private static void removeRestrictedSubkeys(JSONObject jsonObject, Type type) { + if(jsonObject == null) { + return; + } + for(Map.Entry entry : restricted.entrySet()) { + if (type == entry.getValue()) { + JSONObject toCheck = jsonObject.optJSONObject(type.getKey()); + if(toCheck != null && toCheck.has(entry.getKey())) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), entry.getKey()); + jsonObject.remove(entry.getKey()); + } + } + } + } + private static void removeRestrictedSubkeys(JSONArray jsonArray, Type type) { + if(jsonArray == null) { + return; + } + ArrayList toRemove = new ArrayList(); + for(int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject; + if ((jsonObject = jsonArray.optJSONObject(i)) != null) { + for(Map.Entry entry : restricted.entrySet()) { + if (jsonObject.has(entry.getKey()) && type == entry.getValue()) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), entry.getKey()); + toRemove.add(jsonObject); + } + } + } + } + for(Object o : toRemove) { + jsonArray.remove(o); + } + } + public static void sanitize(JSONObject match) throws JSONException { // "options" ~= "config" - System.out.println("BEFORE: " + match); Object cache; - for(RootKey key : RootKey.values()) { + for(Type key : Type.values()) { // Sanitize alts/aliases for(String alt : key.getAlts()) { if ((cache = match.optJSONObject(alt)) != null) { @@ -165,8 +195,6 @@ public static void sanitize(JSONObject match) throws JSONException { } } } - - System.out.println("AFTER: " + match); } private static boolean find(Object base, Object match) throws JSONException { @@ -226,4 +254,8 @@ private static boolean find(Object base, Object match, boolean replace) throws J return match.equals(base); } } + + public static void setRestrictSubstitutions(boolean restrictSubstitutions) { + Substitutions.restrictSubstitutions = restrictSubstitutions; + } } diff --git a/src/qz/ws/substitutions/Type.java b/src/qz/ws/substitutions/Type.java new file mode 100644 index 000000000..e499bd456 --- /dev/null +++ b/src/qz/ws/substitutions/Type.java @@ -0,0 +1,41 @@ +package qz.ws.substitutions; + +public enum Type { + OPTIONS("options", "config"), + PRINTER("printer"), + DATA("data"); + + private String key; + private boolean readOnly; + private String[] alts; + + Type(String key, String ... alts) { + this(key, false, alts); + } + Type(String key, boolean readOnly, String... alts) { + this.key = key; + this.readOnly = readOnly; + this.alts = alts; + } + + public boolean isReadOnly() { + return readOnly; + } + + public static Type parse(Object o) { + for(Type root : values()) { + if (root.key.equals(o)) { + return root; + } + } + return null; + } + + public String getKey() { + return key; + } + + public String[] getAlts() { + return alts; + } +} \ No newline at end of file diff --git a/test/qz/ws/substitutions/SubstitutionsTests.java b/test/qz/ws/substitutions/SubstitutionsTests.java index 5d30ca508..fca998d29 100644 --- a/test/qz/ws/substitutions/SubstitutionsTests.java +++ b/test/qz/ws/substitutions/SubstitutionsTests.java @@ -1,151 +1,18 @@ package qz.ws.substitutions; -import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; -public class SubstitutionsTests { - public static String[] JSON_TEST_MATCH = new String[4]; - public static String[] JSON_TEST_REPLACE = new String[4]; - static { - JSON_TEST_MATCH[0] = "{\n" + - " \"config\": {\n" + - " \"size\": {\n" + - " \"width\": \"4\",\n" + - " \"height\": \"6\"\n" + - " }\n" + - " }\n" + - "}"; - JSON_TEST_MATCH[1] = "{\n" + - " \"printer\": \"PDFwriter\"\n" + - "}"; - JSON_TEST_MATCH[2] = "{\n" + - " \"data\": {\n" + - " \"options\": {\n" + - " \"pageWidth\": \"8.5\",\n" + - " \"pageHeight\": \"11\"\n" + - " }\n" + - " }\n" + - "}"; - - JSON_TEST_MATCH[3] = "{\n" + - " \"config\": {\n" + - " \"copies\": 1" + - " }" + - "}"; - - JSON_TEST_REPLACE[0] = "{\n" + - " \"config\": {\n" + - " \"size\": {\n" + - " \"width\": \"100\",\n" + - " \"height\": \"150\"\n" + - " },\n" + - " \"units\": \"mm\"\n" + - " }\n" + - "}"; - JSON_TEST_REPLACE[1] = "{\n" + - " \"printer\": \"XPS Document Writer\"\n" + - "}"; - JSON_TEST_REPLACE[2] = "{\n" + - " \"data\": {\n" + - " \"options\": {\n" + - " \"pageWidth\": \"8.5\",\n" + - " \"pageHeight\": \"14\"\n" + - " }\n" + - " }\n" + - "}"; - - JSON_TEST_REPLACE[3] = "{\n" + - " \"config\": {\n" + - " \"copies\": 3" + - " }" + - "}"; - } - - public static final String JSON_TEST_BASE = "{\n" + - " \"call\": \"print\",\n" + - " \"params\": {\n" + - " \"printer\": {\n" + - " \"name\": \"PDFwriter\"\n" + - " },\n" + - " \"options\": {\n" + - " \"bounds\": null,\n" + - " \"colorType\": \"color\",\n" + - " \"copies\": 1,\n" + - " \"density\": 0,\n" + - " \"duplex\": false,\n" + - " \"fallbackDensity\": null,\n" + - " \"interpolation\": \"bicubic\",\n" + - " \"jobName\": null,\n" + - " \"legacy\": false,\n" + - " \"margins\": 0,\n" + - " \"orientation\": null,\n" + - " \"paperThickness\": null,\n" + - " \"printerTray\": null,\n" + - " \"rasterize\": false,\n" + - " \"rotation\": 0,\n" + - " \"scaleContent\": true,\n" + - " \"size\": {\n" + - " \"width\": \"4\",\n" + - " \"height\": \"6\"\n" + - " },\n" + - " \"units\": \"in\",\n" + - " \"forceRaw\": false,\n" + - " \"encoding\": null,\n" + - " \"spool\": {}\n" + - " },\n" + - " \"data\": [\n" + - " {\n" + - " \"type\": \"pixel\",\n" + - " \"format\": \"pdf\",\n" + - " \"flavor\": \"file\",\n" + - " \"data\": \"https://demo.qz.io/assets/pdf_sample.pdf\",\n" + - " \"options\": {\n" + - " \"pageWidth\": \"8.5\",\n" + - " \"pageHeight\": \"11\",\n" + - " \"pageRanges\": \"\",\n" + - " \"ignoreTransparency\": false,\n" + - " \"altFontRendering\": false\n" + - " }\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"signature\": \"\",\n" + - " \"timestamp\": 1713895560783,\n" + - " \"uid\": \"64t63d\",\n" + - " \"position\": {\n" + - " \"x\": 720,\n" + - " \"y\": 462.5\n" + - " },\n" + - " \"signAlgorithm\": \"SHA512\"\n" + - "}"; - - public static void main(String ... args) throws JSONException { - JSONArray instructions = new JSONArray(); +import java.io.IOException; - /** TODO: Move the tests to a flat file **/ - JSONObject step1 = new JSONObject(); - step1.put("use", new JSONObject(JSON_TEST_REPLACE[0])); - step1.put("for", new JSONObject(JSON_TEST_MATCH[0])); - - JSONObject step2 = new JSONObject(); - step2.put("use", new JSONObject(JSON_TEST_REPLACE[1])); - step2.put("for", new JSONObject(JSON_TEST_MATCH[1])); - - JSONObject step3 = new JSONObject(); - step3.put("use", new JSONObject(JSON_TEST_REPLACE[2])); - step3.put("for", new JSONObject(JSON_TEST_MATCH[2])); - - // TODO: Why doesn't this test fail on "options" with subkey of "copies" - JSONObject step4 = new JSONObject(); - step4.put("use", new JSONObject(JSON_TEST_REPLACE[3])); - step4.put("for", new JSONObject(JSON_TEST_MATCH[3])); - - instructions.put(step1).put(step2).put(step3).put(step4); - Substitutions substitutions = new Substitutions(instructions.toString()); - - JSONObject base = new JSONObject(JSON_TEST_BASE); - substitutions.replace(base); +public class SubstitutionsTests { + public static void main(String ... args) throws JSONException, IOException { + Substitutions substitutions = new Substitutions( + SubstitutionsTests.class.getResourceAsStream("resources/substitutions.json") + ); + JSONObject base = substitutions.replace( + SubstitutionsTests.class.getResourceAsStream("resources/printRequest.json") + ); System.out.println(base); } diff --git a/test/qz/ws/substitutions/resources/printRequest.json b/test/qz/ws/substitutions/resources/printRequest.json new file mode 100644 index 000000000..9a78ae7e0 --- /dev/null +++ b/test/qz/ws/substitutions/resources/printRequest.json @@ -0,0 +1,57 @@ +{ + "call": "print", + "params": { + "printer": { + "name": "PDFwriter" + }, + "options": { + "bounds": null, + "colorType": "color", + "copies": 1, + "density": 0, + "duplex": false, + "fallbackDensity": null, + "interpolation": "bicubic", + "jobName": null, + "legacy": false, + "margins": 0, + "orientation": null, + "paperThickness": null, + "printerTray": null, + "rasterize": false, + "rotation": 0, + "scaleContent": true, + "size": { + "width": "4", + "height": "6" + }, + "units": "in", + "forceRaw": false, + "encoding": null, + "spool": {} + }, + "data": [ + { + "type": "pixel", + "format": "pdf", + "flavor": "file", + "data": "https://demo.qz.io/assets/pdf_sample.pdf", + "options": { + "pageWidth": "8.5", + "pageHeight": "11", + "pageRanges": "", + "ignoreTransparency": false, + "altFontRendering": false + } + } + ] + }, + "signature": "", + "timestamp": 1713895560783, + "uid": "64t63d", + "position": { + "x": 720, + "y": 462.5 + }, + "signAlgorithm": "SHA512" +}" \ No newline at end of file diff --git a/test/qz/ws/substitutions/resources/substitutions.json b/test/qz/ws/substitutions/resources/substitutions.json new file mode 100644 index 000000000..f9bd482f5 --- /dev/null +++ b/test/qz/ws/substitutions/resources/substitutions.json @@ -0,0 +1,71 @@ +[ + { + "use":{ + "config": { + "size": { + "width": "100", + "height": "150" + }, + "units": "mm" + } + }, + "for": { + "config": { + "size": { + "width": "4", + "height": "6" + } + } + } + }, + { + "use": { + "printer": "XPS Document Writer" + }, + "for": { + "printer": "PDFwriter" + } + }, + { + "use": { + "data": { + "options": { + "pageWidth": "8.5", + "pageHeight": "14" + } + } + }, + "for": { + "data": { + "options": { + "pageWidth": "8.5", + "pageHeight": "11" + } + } + } + }, + { + "use": { + "config": { + "copies": 3 + } + }, + "for": { + "config": { + "copies": 1 + } + } + }, + { + "use": { + "data": { + "data": "https://yahoo.com" + } + }, + "for": { + "data": { + "data": "https://demo.qz.io/assets/pdf_sample.pdf" + } + } + } +] \ No newline at end of file From e329d895d23bc08a95170333d631152249828d04 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Wed, 24 Apr 2024 02:39:10 -0400 Subject: [PATCH 03/17] init() in PrintSocketClient (WIP) --- src/qz/ws/PrintSocketClient.java | 2 ++ src/qz/ws/substitutions/Substitutions.java | 26 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index faa46383b..69ae46917 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -21,6 +21,7 @@ import qz.printer.PrintServiceMatcher; import qz.printer.status.StatusMonitor; import qz.utils.*; +import qz.ws.substitutions.Substitutions; import javax.usb.util.UsbUtil; import java.awt.*; @@ -44,6 +45,7 @@ public class PrintSocketClient { private static final Logger log = LogManager.getLogger(PrintSocketClient.class); private final TrayManager trayManager = PrintSocketServer.getTrayManager(); + private static Substitutions substitutions = Substitutions.init(); private static final Semaphore dialogAvailable = new Semaphore(1, true); //websocket port -> Connection diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index e9783f7f7..a97e9dd11 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -6,10 +6,14 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import qz.utils.FileUtilities; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -18,6 +22,7 @@ public class Substitutions { protected static final Logger log = LogManager.getLogger(Substitutions.class); + private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve("substitutions.json"); // Subkeys that are restricted for writing private static boolean restrictSubstitutions = true; private static HashMap restricted = new HashMap<>(); @@ -27,6 +32,10 @@ public class Substitutions { } private ArrayList matches, replaces; + public Substitutions(Path path) throws IOException, JSONException { + this(new FileInputStream(path.toFile())); + } + public Substitutions(InputStream in) throws IOException, JSONException { this(IOUtils.toString(in, StandardCharsets.UTF_8)); } @@ -98,8 +107,8 @@ public static void replace(JSONObject base, JSONObject replace) throws JSONExcep // Good, let's make sure there are no exceptions if(restrictSubstitutions) { switch(type) { - // Special handling for arrays case DATA: + // Special handling for arrays JSONArray jsonArray = jsonReplace.optJSONArray(type.getKey()); removeRestrictedSubkeys(jsonArray, type); break; @@ -258,4 +267,19 @@ private static boolean find(Object base, Object match, boolean replace) throws J public static void setRestrictSubstitutions(boolean restrictSubstitutions) { Substitutions.restrictSubstitutions = restrictSubstitutions; } + + public static Substitutions init() { + return init(DEFAULT_SUBSTITUTIONS_PATH); + } + public static Substitutions init(Path path) { + Substitutions substitutions = null; + try { + substitutions = new Substitutions(path); + } catch(JSONException e) { + log.warn("Unable to parse substitutions file, skipping", e); + } catch(IOException e) { + log.info("Substitutions file missing, skipping: {}", e.getMessage()); + } + return substitutions; + } } From 626e08dac7ac85efdd4e66c63ad13b5922403ea1 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Wed, 24 Apr 2024 17:33:02 -0400 Subject: [PATCH 04/17] Initial UI support --- src/qz/common/TrayManager.java | 8 + src/qz/ui/AboutDialog.java | 194 ++++++++++++++++----- src/qz/ws/PrintSocketClient.java | 3 +- src/qz/ws/substitutions/Substitutions.java | 5 +- 4 files changed, 168 insertions(+), 42 deletions(-) diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index edb860381..316716af4 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -26,6 +26,7 @@ import qz.utils.*; import qz.ws.PrintSocketServer; import qz.ws.SingleInstanceChecker; +import qz.ws.substitutions.Substitutions; import javax.swing.*; import java.awt.*; @@ -58,6 +59,9 @@ public class TrayManager { // Custom swing pop-up menu private TrayType tray; + // Substitutions reference + private Substitutions substitutions; + private ConfirmDialog confirmDialog; private GatewayDialog gatewayDialog; private AboutDialog aboutDialog; @@ -98,6 +102,9 @@ public TrayManager(boolean isHeadless) { // Set strict certificate mode preference Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE)); + // Configure JSON substitutions + substitutions = Substitutions.init(); + // Set FileIO security FileUtilities.setFileIoEnabled(getPref(SECURITY_FILE_ENABLED)); FileUtilities.setFileIoStrict(getPref(SECURITY_FILE_STRICT)); @@ -542,6 +549,7 @@ public void setServer(Server server, int insecurePortInUse, int securePortInUse) displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server)); if (!headless) { + aboutDialog.setSubstitutions(substitutions); aboutDialog.setServer(server); setDefaultIcon(); } diff --git a/src/qz/ui/AboutDialog.java b/src/qz/ui/AboutDialog.java index df1bce22b..f4d09283e 100644 --- a/src/qz/ui/AboutDialog.java +++ b/src/qz/ui/AboutDialog.java @@ -9,18 +9,23 @@ import qz.ui.component.EmLabel; import qz.ui.component.IconCache; import qz.ui.component.LinkLabel; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; import qz.ws.PrintSocketServer; +import qz.ws.substitutions.Substitutions; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.font.TextAttribute; +import java.awt.datatransfer.DataFlavor; +import java.awt.dnd.*; +import java.io.File; import java.net.URI; import java.net.URL; -import java.util.HashMap; -import java.util.Map; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; /** * Created by Tres on 2/26/2015. @@ -29,16 +34,19 @@ public class AboutDialog extends BasicDialog implements Themeable { private static final Logger log = LogManager.getLogger(AboutDialog.class); - + private final boolean limitedDisplay; private Server server; - - private boolean limitedDisplay; - private JLabel lblUpdate; private JButton updateButton; + private JPanel contentPanel; + private JToolBar headerBar; + private Border dropBorder; + + private Substitutions substitutions; + // Use allows word wrapping on a standard JLabel - class TextWrapLabel extends JLabel { + static class TextWrapLabel extends JLabel { TextWrapLabel(String text) { super("" + text + ""); } @@ -51,6 +59,10 @@ public AboutDialog(JMenuItem menuItem, IconCache iconCache) { limitedDisplay = Constants.VERSION_CHECK_URL.isEmpty(); } + public void setSubstitutions(Substitutions substitutions) { + this.substitutions = substitutions; + } + public void setServer(Server server) { this.server = server; @@ -63,18 +75,10 @@ public void initComponents() { JPanel infoPanel = new JPanel(); infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); - LinkLabel linkLibrary = new LinkLabel("Detailed library information"); - if(server != null && server.isRunning() && !server.isStopping()) { - // Some OSs (e.g. FreeBSD) return null for server.getURI(), fallback to sane values - URI uri = server.getURI(); - String scheme = uri == null ? "http" : uri.getScheme(); - int port = uri == null ? PrintSocketServer.getInsecurePortInUse(): uri.getPort(); - linkLibrary.setLinkLocation(String.format("%s://%s:%s", scheme, AboutInfo.getPreferredHostname(), port)); - } + LinkLabel linkLibrary = getLinkLibrary(); Box versionBox = Box.createHorizontalBox(); versionBox.setAlignmentX(Component.LEFT_ALIGNMENT); - versionBox.add(new JLabel(String.format("%s (Java)", Constants.VERSION.toString()))); - + versionBox.add(new JLabel(String.format("%s (Java)", Constants.VERSION))); JPanel aboutPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); JLabel logo = new JLabel(getIcon(IconCache.Icon.LOGO_ICON)); @@ -126,30 +130,116 @@ public void initComponents() { aboutPanel.add(infoPanel); - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); - panel.add(aboutPanel); - panel.add(new JSeparator()); + contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS)); + contentPanel.add(aboutPanel); + contentPanel.add(new JSeparator()); if (!limitedDisplay) { - LinkLabel lblLicensing = new LinkLabel("Licensing Information", 0.9f, false); - lblLicensing.setLinkLocation(Constants.ABOUT_LICENSING_URL); + contentPanel.add(getSupportPanel()); + } - LinkLabel lblSupport = new LinkLabel("Support Information", 0.9f, false); - lblSupport.setLinkLocation(Constants.ABOUT_SUPPORT_URL); + setContent(contentPanel, true); + contentPanel.setDropTarget(createDropTarget()); + setHeader(headerBar = getHeaderBar()); + refreshHeader(); + } - LinkLabel lblPrivacy = new LinkLabel("Privacy Policy", 0.9f, false); - lblPrivacy.setLinkLocation(Constants.ABOUT_PRIVACY_URL); + private static JPanel getSupportPanel() { + LinkLabel lblLicensing = new LinkLabel("Licensing Information", 0.9f, false); + lblLicensing.setLinkLocation(Constants.ABOUT_LICENSING_URL); - JPanel supportPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 80, 10)); - supportPanel.add(lblLicensing); - supportPanel.add(lblSupport); - supportPanel.add(lblPrivacy); + LinkLabel lblSupport = new LinkLabel("Support Information", 0.9f, false); + lblSupport.setLinkLocation(Constants.ABOUT_SUPPORT_URL); - panel.add(supportPanel); + LinkLabel lblPrivacy = new LinkLabel("Privacy Policy", 0.9f, false); + lblPrivacy.setLinkLocation(Constants.ABOUT_PRIVACY_URL); + + JPanel supportPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 80, 10)); + supportPanel.add(lblLicensing); + supportPanel.add(lblSupport); + supportPanel.add(lblPrivacy); + return supportPanel; + } + + private LinkLabel getLinkLibrary() { + LinkLabel linkLibrary = new LinkLabel("Detailed library information"); + if(server != null && server.isRunning() && !server.isStopping()) { + // Some OSs (e.g. FreeBSD) return null for server.getURI(), fallback to sane values + URI uri = server.getURI(); + String scheme = uri == null ? "http" : uri.getScheme(); + int port = uri == null ? PrintSocketServer.getInsecurePortInUse(): uri.getPort(); + linkLibrary.setLinkLocation(String.format("%s://%s:%s", scheme, AboutInfo.getPreferredHostname(), port)); } + return linkLibrary; + } - setContent(panel, true); + private JToolBar getHeaderBar() { + JToolBar headerBar = new JToolBar(); + headerBar.setBorderPainted(false); + headerBar.setLayout(new FlowLayout()); + headerBar.setOpaque(true); + headerBar.setFloatable(false); + + LinkLabel substitutionsLabel = new LinkLabel("Substitutions are in effect for this machine"); + JButton refreshButton = new JButton("", getIcon(IconCache.Icon.RELOAD_ICON)); + refreshButton.setOpaque(false); + refreshButton.addActionListener(e -> { + refreshSubstitutions(); + refreshHeader(); + }); + + substitutionsLabel.setLinkLocation(FileUtilities.SHARED_DIR.toFile()); + + headerBar.add(substitutionsLabel); + headerBar.add(refreshButton); + return headerBar; + } + + private DropTarget createDropTarget() { + return new DropTarget() { + public synchronized void drop(DropTargetDropEvent evt) { + processDroppedFile(evt); + } + + @Override + public synchronized void dragEnter(DropTargetDragEvent dtde) { + super.dragEnter(dtde); + setDropBorder(true); + } + + @Override + public synchronized void dragExit(DropTargetEvent dte) { + super.dragExit(dte); + setDropBorder(false); + } + }; + } + + private void processDroppedFile(DropTargetDropEvent evt) { + try { + evt.acceptDrop(DnDConstants.ACTION_COPY); + Object dropped = evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + if(dropped instanceof List) { + List droppedFiles = (List)dropped; + for (File file : droppedFiles) { + if(file.getName().equals(Substitutions.FILE_NAME)) { + log.info("File drop accepted: {}", file); + Path source = file.toPath(); + Path dest = FileUtilities.SHARED_DIR.resolve(file.getName()); + Files.copy(source, dest); + FileUtilities.inheritParentPermissions(dest); + refreshSubstitutions(); + refreshHeader(); + break; + } + } + } + evt.dropComplete(true); + } catch (Exception ex) { + log.warn(ex); + } + setDropBorder(false); } private void checkForUpdate() { @@ -157,12 +247,12 @@ private void checkForUpdate() { if (latestVersion.greaterThan(Constants.VERSION)) { lblUpdate.setText("An update is available:"); - updateButton.setText("Download " + latestVersion.toString()); + updateButton.setText("Download " + latestVersion); updateButton.setVisible(true); } else if (latestVersion.lessThan(Constants.VERSION)) { lblUpdate.setText("You are on a beta release."); - updateButton.setText("Revert to stable " + latestVersion.toString()); + updateButton.setText("Revert to stable " + latestVersion); updateButton.setVisible(true); } else { lblUpdate.setText("You have the latest version."); @@ -171,6 +261,28 @@ private void checkForUpdate() { } } + private void setDropBorder(boolean isShown) { + if(isShown) { + if(contentPanel.getBorder() == null) { + dropBorder = BorderFactory.createDashedBorder(Constants.TRUSTED_COLOR, 3, 5, 5, true); + contentPanel.setBorder(dropBorder); + } + } else { + contentPanel.setBorder(null); + } + } + + private void refreshSubstitutions() { + substitutions = Substitutions.init(); + } + + private void refreshHeader() { + headerBar.setBackground(SystemUtilities.isDarkDesktop() ? + Constants.TRUSTED_COLOR.darker().darker() : Constants.TRUSTED_COLOR_DARK); + headerBar.setVisible(substitutions != null); + pack(); + } + @Override public void setVisible(boolean visible) { @@ -181,5 +293,9 @@ public void setVisible(boolean visible) { super.setVisible(visible); } - + @Override + public void refresh() { + refreshHeader(); + super.refresh(); + } } diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index 69ae46917..e7635c16d 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -21,7 +21,6 @@ import qz.printer.PrintServiceMatcher; import qz.printer.status.StatusMonitor; import qz.utils.*; -import qz.ws.substitutions.Substitutions; import javax.usb.util.UsbUtil; import java.awt.*; @@ -45,7 +44,7 @@ public class PrintSocketClient { private static final Logger log = LogManager.getLogger(PrintSocketClient.class); private final TrayManager trayManager = PrintSocketServer.getTrayManager(); - private static Substitutions substitutions = Substitutions.init(); + private static final Semaphore dialogAvailable = new Semaphore(1, true); //websocket port -> Connection diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index a97e9dd11..17af76b0c 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -22,7 +22,10 @@ public class Substitutions { protected static final Logger log = LogManager.getLogger(Substitutions.class); - private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve("substitutions.json"); + public static final String FILE_NAME = "substitutions.json"; + + private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve(FILE_NAME); + // Subkeys that are restricted for writing private static boolean restrictSubstitutions = true; private static HashMap restricted = new HashMap<>(); From 74beb1127116bc0000582df0e54ffcf9254c28e0 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 25 Apr 2024 16:45:10 -0400 Subject: [PATCH 05/17] Add restrictions property/pref, case sensitivity, CLI usage --- src/qz/App.java | 2 + src/qz/common/TrayManager.java | 2 +- src/qz/utils/ArgParser.java | 2 +- src/qz/utils/ArgValue.java | 4 ++ src/qz/ws/substitutions/Substitutions.java | 47 +++++++++++++------ .../resources/substitutions.json | 24 +++++++--- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/qz/App.java b/src/qz/App.java index 39f31a81e..42d1159b9 100644 --- a/src/qz/App.java +++ b/src/qz/App.java @@ -21,6 +21,7 @@ import qz.utils.*; import qz.ws.PrintSocketServer; import qz.ws.SingleInstanceChecker; +import qz.ws.substitutions.Substitutions; import java.io.File; import java.security.cert.X509Certificate; @@ -43,6 +44,7 @@ public static void main(String ... args) { log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY); log.info("Java version: {}", Constants.JAVA_VERSION.toString()); log.info("Java vendor: {}", Constants.JAVA_VENDOR); + Substitutions.setRestrictSubstitutions(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_RESTRICT)); CertificateManager certManager = null; try { diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index 316716af4..c5bb2c53c 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -102,7 +102,7 @@ public TrayManager(boolean isHeadless) { // Set strict certificate mode preference Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE)); - // Configure JSON substitutions + // Configure JSON substitutions = Substitutions.init(); // Set FileIO security diff --git a/src/qz/utils/ArgParser.java b/src/qz/utils/ArgParser.java index 1d7512dd2..dae8854b3 100644 --- a/src/qz/utils/ArgParser.java +++ b/src/qz/utils/ArgParser.java @@ -51,7 +51,7 @@ public int getCode() { private static final String USAGE_COMMAND = String.format("java -jar %s.jar", PROPS_FILE); private static final String USAGE_COMMAND_PARAMETER = String.format("java -Dfoo.bar= -jar %s.jar", PROPS_FILE); - private static final int DESCRIPTION_COLUMN = 30; + private static final int DESCRIPTION_COLUMN = 35; private static final int INDENT_SIZE = 2; private List args; diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index cfe0856d7..60f5fb74f 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -71,6 +71,10 @@ public enum ArgValue { "security.file.enabled"), SECURITY_FILE_STRICT(PREFERENCES, "Enable/disable signing requirements for File Communications features", null, true, "security.file.strict"), + + SECURITY_SUBSTITUTIONS_RESTRICT(PREFERENCES, "Enable/disable JSON substitution restrictions such as copies, data blobs", null, true, + "security.substitutions.restrict"), + SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https", "security.data.protocols"), SECURITY_PRINT_TOFILE(PREFERENCES, "Enable/disable printing directly to file paths", null, false, diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 17af76b0c..261bb8e5c 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -9,21 +9,18 @@ import qz.utils.FileUtilities; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.*; public class Substitutions { protected static final Logger log = LogManager.getLogger(Substitutions.class); public static final String FILE_NAME = "substitutions.json"; + private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve(FILE_NAME); // Subkeys that are restricted for writing @@ -34,6 +31,7 @@ public class Substitutions { restricted.put("data", Type.DATA); } private ArrayList matches, replaces; + private ArrayList matchCase; public Substitutions(Path path) throws IOException, JSONException { this(new FileInputStream(path.toFile())); @@ -45,6 +43,7 @@ public Substitutions(InputStream in) throws IOException, JSONException { public Substitutions(String serialized) throws JSONException { matches = new ArrayList<>(); + matchCase = new ArrayList<>(); replaces = new ArrayList<>(); JSONArray instructions = new JSONArray(serialized); @@ -59,11 +58,14 @@ public Substitutions(String serialized) throws JSONException { JSONObject match = step.optJSONObject("for"); if(match != null) { + this.matchCase.add(match.optBoolean("caseSensitive", false)); + match.remove("caseSensitive"); sanitize(match); this.matches.add(match); } } } + if(matches.size() != replaces.size()) { throw new SubstitutionException("Mismatched instructions; Each \"use\" must have a matching \"for\"."); } @@ -75,11 +77,17 @@ public JSONObject replace(InputStream in) throws IOException, JSONException { public JSONObject replace(JSONObject base) throws JSONException { for(int i = 0; i < matches.size(); i++) { - if (find(base, matches.get(i))) { - log.debug("Matched JSON substitution rule: for: {}, use: {}", matches.get(i), replaces.get(i)); + if (find(base, matches.get(i), matchCase.get(i))) { + log.debug("Matched {}JSON substitution rule: for: {}, use: {}", + matchCase.get(i) ? "case-sensitive " : "", + matches.get(i), + replaces.get(i)); replace(base, replaces.get(i)); } else { - log.debug("Unable to match substitution rule: for: {}, use: {}", matches.get(i), replaces.get(i)); + log.debug("Unable to match {}JSON substitution rule: for: {}, use: {}", + matchCase.get(i) ? "case-sensitive " : "", + matches.get(i), + replaces.get(i)); } } return base; @@ -121,7 +129,7 @@ public static void replace(JSONObject base, JSONObject replace) throws JSONExcep } } } - find(base, replace, true); + find(base, replace, false, true); } private static void removeRestrictedSubkeys(JSONObject jsonObject, Type type) { @@ -209,10 +217,10 @@ public static void sanitize(JSONObject match) throws JSONException { } } - private static boolean find(Object base, Object match) throws JSONException { - return find(base, match, false); + private static boolean find(Object base, Object match, boolean caseSensitive) throws JSONException { + return find(base, match, caseSensitive, false); } - private static boolean find(Object base, Object match, boolean replace) throws JSONException { + private static boolean find(Object base, Object match, boolean caseSensitive, boolean replace) throws JSONException { if(base instanceof JSONObject) { if(match instanceof JSONObject) { JSONObject jsonMatch = (JSONObject)match; @@ -229,7 +237,7 @@ private static boolean find(Object base, Object match, boolean replace) throws J // Overwrite value, don't recurse jsonBase.put(next.toString(), newMatch); continue; - } else if(find(newBase, newMatch, replace)) { + } else if(find(newBase, newMatch, caseSensitive, replace)) { continue; } } else if(replace) { @@ -251,7 +259,7 @@ private static boolean find(Object base, Object match, boolean replace) throws J Object newMatch = matchArray.get(i); for(int j = 0; j < baseArray.length(); j++) { Object newBase = baseArray.get(j); - if(find(newBase, newMatch, replace)) { + if(find(newBase, newMatch, caseSensitive, replace)) { continue match; } } @@ -263,7 +271,16 @@ private static boolean find(Object base, Object match, boolean replace) throws J } } else { // Treat as primitives - return match.equals(base); + if(!match.getClass().getName().equals("java.lang.String") && match.getClass().equals(base.getClass())) { + // Same type + return match.equals(base); + } else { + // Dissimilar types (e.g. "width": "8.5" versus "width": 8.5), cast both to String + if(caseSensitive) { + return match.toString().equals(base.toString()); + } + return match.toString().equalsIgnoreCase(base.toString()); + } } } diff --git a/test/qz/ws/substitutions/resources/substitutions.json b/test/qz/ws/substitutions/resources/substitutions.json index f9bd482f5..aff629d8d 100644 --- a/test/qz/ws/substitutions/resources/substitutions.json +++ b/test/qz/ws/substitutions/resources/substitutions.json @@ -3,8 +3,8 @@ "use":{ "config": { "size": { - "width": "100", - "height": "150" + "width": 100, + "height": 150 }, "units": "mm" } @@ -12,9 +12,10 @@ "for": { "config": { "size": { - "width": "4", - "height": "6" - } + "width": 4, + "height": 6 + }, + "units": "in" } } }, @@ -30,8 +31,8 @@ "use": { "data": { "options": { - "pageWidth": "8.5", - "pageHeight": "14" + "pageWidth": 8.5, + "pageHeight": 14 } } }, @@ -56,6 +57,15 @@ } } }, + { + "use": { + "printer": "PDFwriter" + }, + "for": { + "caseSensitive": true, + "printer": "xps document writer" + } + }, { "use": { "data": { From 2d1caa6903e102941e5bd2ff2fe68a79a87afdf7 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 14:37:53 -0400 Subject: [PATCH 06/17] Activate substitutions --- src/qz/common/TrayManager.java | 9 +--- src/qz/ui/AboutDialog.java | 41 +++++++++++++------ src/qz/ws/PrintSocketClient.java | 7 ++++ src/qz/ws/substitutions/Substitutions.java | 29 +++++++++++-- .../resources/substitutions.json | 2 +- 5 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index c5bb2c53c..eb2818f95 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -58,10 +58,6 @@ public class TrayManager { // Custom swing pop-up menu private TrayType tray; - - // Substitutions reference - private Substitutions substitutions; - private ConfirmDialog confirmDialog; private GatewayDialog gatewayDialog; private AboutDialog aboutDialog; @@ -102,8 +98,8 @@ public TrayManager(boolean isHeadless) { // Set strict certificate mode preference Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE)); - // Configure JSON - substitutions = Substitutions.init(); + // Configures JSON websocket messages + Substitutions.getInstance(); // Set FileIO security FileUtilities.setFileIoEnabled(getPref(SECURITY_FILE_ENABLED)); @@ -549,7 +545,6 @@ public void setServer(Server server, int insecurePortInUse, int securePortInUse) displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server)); if (!headless) { - aboutDialog.setSubstitutions(substitutions); aboutDialog.setServer(server); setDefaultIcon(); } diff --git a/src/qz/ui/AboutDialog.java b/src/qz/ui/AboutDialog.java index f4d09283e..4b1f494de 100644 --- a/src/qz/ui/AboutDialog.java +++ b/src/qz/ui/AboutDialog.java @@ -25,7 +25,11 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; /** * Created by Tres on 2/26/2015. @@ -43,8 +47,6 @@ public class AboutDialog extends BasicDialog implements Themeable { private JToolBar headerBar; private Border dropBorder; - private Substitutions substitutions; - // Use allows word wrapping on a standard JLabel static class TextWrapLabel extends JLabel { TextWrapLabel(String text) { @@ -59,10 +61,6 @@ public AboutDialog(JMenuItem menuItem, IconCache iconCache) { limitedDisplay = Constants.VERSION_CHECK_URL.isEmpty(); } - public void setSubstitutions(Substitutions substitutions) { - this.substitutions = substitutions; - } - public void setServer(Server server) { this.server = server; @@ -185,7 +183,7 @@ private JToolBar getHeaderBar() { JButton refreshButton = new JButton("", getIcon(IconCache.Icon.RELOAD_ICON)); refreshButton.setOpaque(false); refreshButton.addActionListener(e -> { - refreshSubstitutions(); + Substitutions.getInstance(true); refreshHeader(); }); @@ -224,14 +222,18 @@ private void processDroppedFile(DropTargetDropEvent evt) { List droppedFiles = (List)dropped; for (File file : droppedFiles) { if(file.getName().equals(Substitutions.FILE_NAME)) { + blinkDropBorder(true); log.info("File drop accepted: {}", file); Path source = file.toPath(); Path dest = FileUtilities.SHARED_DIR.resolve(file.getName()); - Files.copy(source, dest); + Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING); FileUtilities.inheritParentPermissions(dest); - refreshSubstitutions(); + Substitutions.getInstance(true); refreshHeader(); break; + } else { + blinkDropBorder(false); + break; } } } @@ -272,14 +274,29 @@ private void setDropBorder(boolean isShown) { } } - private void refreshSubstitutions() { - substitutions = Substitutions.init(); + private void blinkDropBorder(boolean success) { + Color borderColor = success ? Color.GREEN : Constants.WARNING_COLOR; + dropBorder = BorderFactory.createDashedBorder(borderColor, 3, 5, 5, true); + AtomicBoolean toggled = new AtomicBoolean(true); + int blinkCount = 3; + int blinkDelay = 100; // ms + for(int i = 0; i < blinkCount * 2; i++) { + Timer timer = new Timer("blink" + i); + timer.schedule(new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { + contentPanel.setBorder(toggled.getAndSet(!toggled.get())? dropBorder:null); + }); + } + }, i * blinkDelay); + } } private void refreshHeader() { headerBar.setBackground(SystemUtilities.isDarkDesktop() ? Constants.TRUSTED_COLOR.darker().darker() : Constants.TRUSTED_COLOR_DARK); - headerBar.setVisible(substitutions != null); + headerBar.setVisible(Substitutions.getInstance() != null); pack(); } diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index e7635c16d..a1ea829ad 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -21,6 +21,7 @@ import qz.printer.PrintServiceMatcher; import qz.printer.status.StatusMonitor; import qz.utils.*; +import qz.ws.substitutions.Substitutions; import javax.usb.util.UsbUtil; import java.awt.*; @@ -225,6 +226,12 @@ private boolean validSignature(Certificate certificate, JSONObject message) thro * @param json JSON received from web API */ private void processMessage(Session session, JSONObject json, SocketConnection connection, RequestState request) throws JSONException, SerialPortException, DeviceException, IOException { + // perform client-side substitutions + Substitutions substitutions = Substitutions.getInstance(); + if(substitutions != null) { + json = substitutions.replace(json); + } + String UID = json.optString("uid"); SocketMethod call = SocketMethod.findFromCall(json.optString("call")); JSONObject params = json.optJSONObject("params"); diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 261bb8e5c..0dd153a46 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -32,6 +32,7 @@ public class Substitutions { } private ArrayList matches, replaces; private ArrayList matchCase; + private static Substitutions INSTANCE; public Substitutions(Path path) throws IOException, JSONException { this(new FileInputStream(path.toFile())); @@ -288,13 +289,24 @@ public static void setRestrictSubstitutions(boolean restrictSubstitutions) { Substitutions.restrictSubstitutions = restrictSubstitutions; } - public static Substitutions init() { - return init(DEFAULT_SUBSTITUTIONS_PATH); + /** + * Returns a new instance of the Substitutions object from the default + * substitutions.json file at DEFAULT_SUBSTITUTIONS_PATH, + * or null if an error occurred. + */ + public static Substitutions newInstance() { + return newInstance(DEFAULT_SUBSTITUTIONS_PATH); } - public static Substitutions init(Path path) { + + /** + * Returns a new instance of the Substitutions object from the provided + * json substitutions file, or null if an error occurred. + */ + public static Substitutions newInstance(Path path) { Substitutions substitutions = null; try { substitutions = new Substitutions(path); + log.info("Successfully parsed new substitutions file."); } catch(JSONException e) { log.warn("Unable to parse substitutions file, skipping", e); } catch(IOException e) { @@ -302,4 +314,15 @@ public static Substitutions init(Path path) { } return substitutions; } + + public static Substitutions getInstance() { + return getInstance(false); + } + + public static Substitutions getInstance(boolean forceRefresh) { + if(INSTANCE == null || forceRefresh) { + INSTANCE = Substitutions.newInstance(); + } + return INSTANCE; + } } diff --git a/test/qz/ws/substitutions/resources/substitutions.json b/test/qz/ws/substitutions/resources/substitutions.json index aff629d8d..e45e1f4f9 100644 --- a/test/qz/ws/substitutions/resources/substitutions.json +++ b/test/qz/ws/substitutions/resources/substitutions.json @@ -21,7 +21,7 @@ }, { "use": { - "printer": "XPS Document Writer" + "printer": "PDF" }, "for": { "printer": "PDFwriter" From 110bd5df661a31919bf6e17edde3aa06651f97eb Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 15:18:16 -0400 Subject: [PATCH 07/17] Add support for "query": keyword. --- src/qz/ws/substitutions/Type.java | 3 ++- test/qz/ws/substitutions/resources/substitutions.json | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/qz/ws/substitutions/Type.java b/src/qz/ws/substitutions/Type.java index e499bd456..49656642c 100644 --- a/src/qz/ws/substitutions/Type.java +++ b/src/qz/ws/substitutions/Type.java @@ -3,7 +3,8 @@ public enum Type { OPTIONS("options", "config"), PRINTER("printer"), - DATA("data"); + DATA("data"), + QUERY("query"); private String key; private boolean readOnly; diff --git a/test/qz/ws/substitutions/resources/substitutions.json b/test/qz/ws/substitutions/resources/substitutions.json index e45e1f4f9..d77f38397 100644 --- a/test/qz/ws/substitutions/resources/substitutions.json +++ b/test/qz/ws/substitutions/resources/substitutions.json @@ -45,6 +45,14 @@ } } }, + { + "use": { + "query": "pdf" + }, + "for": { + "query": "zzz" + } + }, { "use": { "config": { From 9cb6f8910dcdc068a04c8c39b090420219fa64b4 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 19:29:51 -0400 Subject: [PATCH 08/17] Add property to enable/disable globally --- src/qz/App.java | 1 + src/qz/ui/AboutDialog.java | 2 +- src/qz/utils/ArgValue.java | 6 +++++- src/qz/ws/PrintSocketClient.java | 8 +++++--- src/qz/ws/substitutions/Substitutions.java | 15 +++++++++++++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/qz/App.java b/src/qz/App.java index 42d1159b9..f65cb943b 100644 --- a/src/qz/App.java +++ b/src/qz/App.java @@ -44,6 +44,7 @@ public static void main(String ... args) { log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY); log.info("Java version: {}", Constants.JAVA_VERSION.toString()); log.info("Java vendor: {}", Constants.JAVA_VENDOR); + Substitutions.setEnabled(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_ENABLE)); Substitutions.setRestrictSubstitutions(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_RESTRICT)); CertificateManager certManager = null; diff --git a/src/qz/ui/AboutDialog.java b/src/qz/ui/AboutDialog.java index 4b1f494de..e9ea8d696 100644 --- a/src/qz/ui/AboutDialog.java +++ b/src/qz/ui/AboutDialog.java @@ -296,7 +296,7 @@ public void run() { private void refreshHeader() { headerBar.setBackground(SystemUtilities.isDarkDesktop() ? Constants.TRUSTED_COLOR.darker().darker() : Constants.TRUSTED_COLOR_DARK); - headerBar.setVisible(Substitutions.getInstance() != null); + headerBar.setVisible(Substitutions.areActive()); pack(); } diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index 60f5fb74f..5d9a52206 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -1,6 +1,7 @@ package qz.utils; import qz.common.Constants; +import qz.ws.substitutions.Substitutions; import java.util.ArrayList; import java.util.Arrays; @@ -72,7 +73,10 @@ public enum ArgValue { SECURITY_FILE_STRICT(PREFERENCES, "Enable/disable signing requirements for File Communications features", null, true, "security.file.strict"), - SECURITY_SUBSTITUTIONS_RESTRICT(PREFERENCES, "Enable/disable JSON substitution restrictions such as copies, data blobs", null, true, + SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, + "security.substitutions.allow"), + + SECURITY_SUBSTITUTIONS_RESTRICT(PREFERENCES, "Enable/disable JSON substitution restrictions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, "security.substitutions.restrict"), SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https", diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index a1ea829ad..b6594ff13 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -227,9 +227,11 @@ private boolean validSignature(Certificate certificate, JSONObject message) thro */ private void processMessage(Session session, JSONObject json, SocketConnection connection, RequestState request) throws JSONException, SerialPortException, DeviceException, IOException { // perform client-side substitutions - Substitutions substitutions = Substitutions.getInstance(); - if(substitutions != null) { - json = substitutions.replace(json); + if(Substitutions.areActive()) { + Substitutions substitutions = Substitutions.getInstance(); + if (substitutions != null) { + json = substitutions.replace(json); + } } String UID = json.optString("uid"); diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 0dd153a46..62bf63ea2 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -6,6 +6,7 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import qz.utils.ArgValue; import qz.utils.FileUtilities; import java.io.FileInputStream; @@ -23,6 +24,9 @@ public class Substitutions { private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve(FILE_NAME); + // Global toggle (should match ArgValue.SECURITY_SUBSTITUTIONS_ENABLE) + private static boolean enabled = true; + // Subkeys that are restricted for writing private static boolean restrictSubstitutions = true; private static HashMap restricted = new HashMap<>(); @@ -285,6 +289,10 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo } } + public static void setEnabled(boolean enabled) { + Substitutions.enabled = enabled; + } + public static void setRestrictSubstitutions(boolean restrictSubstitutions) { Substitutions.restrictSubstitutions = restrictSubstitutions; } @@ -322,7 +330,14 @@ public static Substitutions getInstance() { public static Substitutions getInstance(boolean forceRefresh) { if(INSTANCE == null || forceRefresh) { INSTANCE = Substitutions.newInstance(); + if(!enabled) { + log.warn("Substitution file was found, but substitutions are currently disabled via \"{}=false\"", ArgValue.SECURITY_SUBSTITUTIONS_ENABLE.getMatch()); + } } return INSTANCE; } + + public static boolean areActive() { + return Substitutions.getInstance() != null && enabled; + } } From 03f5c1eddbc09170158424b7dcec745651a6325f Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 19:40:54 -0400 Subject: [PATCH 09/17] Restrict -> strict --- src/qz/App.java | 2 +- src/qz/utils/ArgValue.java | 4 ++-- src/qz/ws/substitutions/Substitutions.java | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/qz/App.java b/src/qz/App.java index f65cb943b..8d4c38904 100644 --- a/src/qz/App.java +++ b/src/qz/App.java @@ -45,7 +45,7 @@ public static void main(String ... args) { log.info("Java version: {}", Constants.JAVA_VERSION.toString()); log.info("Java vendor: {}", Constants.JAVA_VENDOR); Substitutions.setEnabled(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_ENABLE)); - Substitutions.setRestrictSubstitutions(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_RESTRICT)); + Substitutions.setStrict(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_STRICT)); CertificateManager certManager = null; try { diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index 5d9a52206..7d96594e0 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -76,8 +76,8 @@ public enum ArgValue { SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, "security.substitutions.allow"), - SECURITY_SUBSTITUTIONS_RESTRICT(PREFERENCES, "Enable/disable JSON substitution restrictions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, - "security.substitutions.restrict"), + SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, + "security.substitutions.strict"), SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https", "security.data.protocols"), diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 62bf63ea2..3550ea8c8 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -27,12 +27,12 @@ public class Substitutions { // Global toggle (should match ArgValue.SECURITY_SUBSTITUTIONS_ENABLE) private static boolean enabled = true; - // Subkeys that are restricted for writing - private static boolean restrictSubstitutions = true; - private static HashMap restricted = new HashMap<>(); + // Subkeys that are restricted for writing because they can materially impact the content + private static boolean strict = true; + private static HashMap parlous = new HashMap<>(); static { - restricted.put("copies", Type.OPTIONS); - restricted.put("data", Type.DATA); + parlous.put("copies", Type.OPTIONS); + parlous.put("data", Type.DATA); } private ArrayList matches, replaces; private ArrayList matchCase; @@ -121,7 +121,7 @@ public static void replace(JSONObject base, JSONObject replace) throws JSONExcep Type type = Type.parse(it.next()); if(type != null && !type.isReadOnly()) { // Good, let's make sure there are no exceptions - if(restrictSubstitutions) { + if(strict) { switch(type) { case DATA: // Special handling for arrays @@ -141,7 +141,7 @@ private static void removeRestrictedSubkeys(JSONObject jsonObject, Type type) { if(jsonObject == null) { return; } - for(Map.Entry entry : restricted.entrySet()) { + for(Map.Entry entry : parlous.entrySet()) { if (type == entry.getValue()) { JSONObject toCheck = jsonObject.optJSONObject(type.getKey()); if(toCheck != null && toCheck.has(entry.getKey())) { @@ -159,7 +159,7 @@ private static void removeRestrictedSubkeys(JSONArray jsonArray, Type type) { for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject; if ((jsonObject = jsonArray.optJSONObject(i)) != null) { - for(Map.Entry entry : restricted.entrySet()) { + for(Map.Entry entry : parlous.entrySet()) { if (jsonObject.has(entry.getKey()) && type == entry.getValue()) { log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), entry.getKey()); toRemove.add(jsonObject); @@ -293,8 +293,8 @@ public static void setEnabled(boolean enabled) { Substitutions.enabled = enabled; } - public static void setRestrictSubstitutions(boolean restrictSubstitutions) { - Substitutions.restrictSubstitutions = restrictSubstitutions; + public static void setStrict(boolean strict) { + Substitutions.strict = strict; } /** From c13fa48c43dbce2387dbbc362b4cafee72b4195d Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 19:43:57 -0400 Subject: [PATCH 10/17] Allow -> enable --- src/qz/utils/ArgValue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index 7d96594e0..fb3fb63ae 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -74,7 +74,7 @@ public enum ArgValue { "security.file.strict"), SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, - "security.substitutions.allow"), + "security.substitutions.enable"), SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, "security.substitutions.strict"), From fa2e03c7d2f0eb9d41c922910fe5f46bd73da170 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Fri, 26 Apr 2024 20:24:51 -0400 Subject: [PATCH 11/17] Don't let sanitize clobber multiple rules --- src/qz/utils/ArgValue.java | 1 - src/qz/ws/substitutions/Substitutions.java | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index fb3fb63ae..2fb61fb40 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -75,7 +75,6 @@ public enum ArgValue { SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, "security.substitutions.enable"), - SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, "security.substitutions.strict"), diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 3550ea8c8..b81a1ad86 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -177,6 +177,7 @@ public static void sanitize(JSONObject match) throws JSONException { Object cache; + JSONObject nested = new JSONObject(); for(Type key : Type.values()) { // Sanitize alts/aliases for(String alt : key.getAlts()) { @@ -189,7 +190,6 @@ public static void sanitize(JSONObject match) throws JSONException { // Special handling for nesting of "printer", "options", "data" within "params" if((cache = match.opt(key.getKey())) != null) { - JSONObject nested = new JSONObject(); switch(key) { case PRINTER: JSONObject name = new JSONObject(); @@ -199,11 +199,12 @@ public static void sanitize(JSONObject match) throws JSONException { default: nested.put(key.getKey(), cache); } - - match.put("params", nested); match.remove(key.getKey()); } } + if(nested.length() > 0) { + match.put("params", nested); + } // Special handling for "data" being provided as an object instead of an array if((cache = match.opt("params")) != null) { From 0ece3ceb0a11af4de8fe96fd9c02bbf18a841416 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Mon, 29 Apr 2024 10:49:15 -0400 Subject: [PATCH 12/17] Whitespace --- src/qz/common/TrayManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index eb2818f95..ba1ac2b53 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -58,6 +58,7 @@ public class TrayManager { // Custom swing pop-up menu private TrayType tray; + private ConfirmDialog confirmDialog; private GatewayDialog gatewayDialog; private AboutDialog aboutDialog; From d691aaa1f137584fde1b2943c89d6c52881b1a51 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Mon, 29 Apr 2024 10:51:37 -0400 Subject: [PATCH 13/17] Wording --- src/qz/utils/ArgValue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java index 2fb61fb40..818a4e11d 100644 --- a/src/qz/utils/ArgValue.java +++ b/src/qz/utils/ArgValue.java @@ -75,7 +75,7 @@ public enum ArgValue { SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, "security.substitutions.enable"), - SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, + SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable/disable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, "security.substitutions.strict"), SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https", From 9ed33ff139bc062acfef67c01190844a790cd458 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Mon, 29 Apr 2024 14:55:20 -0400 Subject: [PATCH 14/17] Better number comparisons --- src/qz/utils/ByteUtilities.java | 17 +++++++++++++++++ src/qz/ws/substitutions/Substitutions.java | 20 +++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/qz/utils/ByteUtilities.java b/src/qz/utils/ByteUtilities.java index 935a6d7ce..ca9a5ee10 100644 --- a/src/qz/utils/ByteUtilities.java +++ b/src/qz/utils/ByteUtilities.java @@ -272,4 +272,21 @@ public static int[] unwind(int bitwiseCode) { return matches; } + public static boolean numberEquals(Object val1, Object val2) { + try { + if(val1 == null) { + return val2 == null; + } else if(val1.getClass().getName() == val2.getClass().getName()) { + return val1.equals(val2); + } else if(val1 instanceof Long) { + return val1.equals(Long.parseLong(val2.toString())); + } else { + return Double.parseDouble(val1.toString()) == Double.parseDouble(val2.toString()); + } + } catch(NumberFormatException nfe) { + log.warn("Cannot not compare [{} = '{}']. Reason: {} {}", val1, val2, nfe.getClass().getName(), nfe.getMessage()); + } + return false; + } + } diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index b81a1ad86..478b4be93 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -7,6 +7,7 @@ import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import qz.utils.ArgValue; +import qz.utils.ByteUtilities; import qz.utils.FileUtilities; import java.io.FileInputStream; @@ -277,16 +278,17 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo } } else { // Treat as primitives - if(!match.getClass().getName().equals("java.lang.String") && match.getClass().equals(base.getClass())) { - // Same type - return match.equals(base); - } else { - // Dissimilar types (e.g. "width": "8.5" versus "width": 8.5), cast both to String - if(caseSensitive) { - return match.toString().equals(base.toString()); - } - return match.toString().equalsIgnoreCase(base.toString()); + if(match instanceof Number) { + return ByteUtilities.numberEquals(match, base); + } + if(base instanceof Number) { + return ByteUtilities.numberEquals(base, match); + } + // Fallback: Cast both to String + if(caseSensitive) { + return match.toString().equals(base.toString()); } + return match.toString().equalsIgnoreCase(base.toString()); } } From 98d72a0e15b5113be7a07e181979282b03c000b4 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Tue, 30 Apr 2024 23:56:33 -0400 Subject: [PATCH 15/17] Fix array element replacements, simplify number comparison --- src/qz/utils/ByteUtilities.java | 8 +++++--- src/qz/ws/substitutions/Substitutions.java | 16 +++++++-------- .../substitutions/resources/printRequest.json | 14 +++++++++++-- .../resources/substitutions.json | 20 +++++++++++++++++++ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/qz/utils/ByteUtilities.java b/src/qz/utils/ByteUtilities.java index ca9a5ee10..acba8baf5 100644 --- a/src/qz/utils/ByteUtilities.java +++ b/src/qz/utils/ByteUtilities.java @@ -274,12 +274,14 @@ public static int[] unwind(int bitwiseCode) { public static boolean numberEquals(Object val1, Object val2) { try { - if(val1 == null) { - return val2 == null; - } else if(val1.getClass().getName() == val2.getClass().getName()) { + if(val1 == null || val2 == null) { + return val1 == val2; + } else if(val1.getClass() == val2.getClass()) { return val1.equals(val2); } else if(val1 instanceof Long) { return val1.equals(Long.parseLong(val2.toString())); + } else if(val2 instanceof Long) { + return val2.equals(Long.parseLong(val1.toString())); } else { return Double.parseDouble(val1.toString()) == Double.parseDouble(val2.toString()); } diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 478b4be93..51a4ebbe2 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -258,6 +258,7 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo return false; // mismatched types } } else if (base instanceof JSONArray) { + boolean found = false; if(match instanceof JSONArray) { JSONArray matchArray = (JSONArray)match; JSONArray baseArray = (JSONArray)base; @@ -267,23 +268,20 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo for(int j = 0; j < baseArray.length(); j++) { Object newBase = baseArray.get(j); if(find(newBase, newMatch, caseSensitive, replace)) { - continue match; + found = true; + if(!replace) { + continue match; + } } } - return false; } - return true; // assume found - } else { - return false; } + return found; } else { // Treat as primitives - if(match instanceof Number) { + if(match instanceof Number || base instanceof Number) { return ByteUtilities.numberEquals(match, base); } - if(base instanceof Number) { - return ByteUtilities.numberEquals(base, match); - } // Fallback: Cast both to String if(caseSensitive) { return match.toString().equals(base.toString()); diff --git a/test/qz/ws/substitutions/resources/printRequest.json b/test/qz/ws/substitutions/resources/printRequest.json index 9a78ae7e0..98612c289 100644 --- a/test/qz/ws/substitutions/resources/printRequest.json +++ b/test/qz/ws/substitutions/resources/printRequest.json @@ -43,7 +43,17 @@ "ignoreTransparency": false, "altFontRendering": false } - } + }, + { + "type": "pixel", + "format": "image", + "flavor": "file", + "data": "https://demo.qz.io/assets/img/image_sample.png", + }, + "^XA\n", + "^FO50,50^ADN,36,20^FDPRINTED WITH QZ 2.2.4-SNAPSHOT\n", + "^FS\n", + "^XZ\n" ] }, "signature": "", @@ -54,4 +64,4 @@ "y": 462.5 }, "signAlgorithm": "SHA512" -}" \ No newline at end of file +} \ No newline at end of file diff --git a/test/qz/ws/substitutions/resources/substitutions.json b/test/qz/ws/substitutions/resources/substitutions.json index d77f38397..274cfe5a3 100644 --- a/test/qz/ws/substitutions/resources/substitutions.json +++ b/test/qz/ws/substitutions/resources/substitutions.json @@ -85,5 +85,25 @@ "data": "https://demo.qz.io/assets/pdf_sample.pdf" } } + }, + { + "use": { + "printer": "ZDesigner" + }, + "for": { + "data": [ "^XA\n" ] + } + }, + { + "use": { + "data": { + "type": "PIXEL" + } + }, + "for": { + "data": { + "type": "pixel" + } + } } ] \ No newline at end of file From c41d053d0851d245e34bf0dad9a491b88bf6b3f0 Mon Sep 17 00:00:00 2001 From: Vzor- Date: Thu, 2 May 2024 00:58:15 -0400 Subject: [PATCH 16/17] Refactor --- src/qz/ws/substitutions/Substitutions.java | 155 +++++++++++---------- 1 file changed, 81 insertions(+), 74 deletions(-) diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index 51a4ebbe2..deb383ea7 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -10,10 +10,10 @@ import qz.utils.ByteUtilities; import qz.utils.FileUtilities; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -30,17 +30,16 @@ public class Substitutions { // Subkeys that are restricted for writing because they can materially impact the content private static boolean strict = true; - private static HashMap parlous = new HashMap<>(); + private static final HashMap parlous = new HashMap<>(); static { - parlous.put("copies", Type.OPTIONS); - parlous.put("data", Type.DATA); + parlous.put(Type.OPTIONS, new String[] {"copies"}); + parlous.put(Type.DATA, new String[] {"data"}); } - private ArrayList matches, replaces; - private ArrayList matchCase; + private final ArrayList rules; private static Substitutions INSTANCE; public Substitutions(Path path) throws IOException, JSONException { - this(new FileInputStream(path.toFile())); + this(Files.newInputStream(path.toFile().toPath())); } public Substitutions(InputStream in) throws IOException, JSONException { @@ -48,33 +47,15 @@ public Substitutions(InputStream in) throws IOException, JSONException { } public Substitutions(String serialized) throws JSONException { - matches = new ArrayList<>(); - matchCase = new ArrayList<>(); - replaces = new ArrayList<>(); + rules = new ArrayList<>(); JSONArray instructions = new JSONArray(serialized); for(int i = 0; i < instructions.length(); i++) { JSONObject step = instructions.optJSONObject(i); if(step != null) { - JSONObject replace = step.optJSONObject("use"); - if(replace != null) { - sanitize(replace); - this.replaces.add(replace); - } - - JSONObject match = step.optJSONObject("for"); - if(match != null) { - this.matchCase.add(match.optBoolean("caseSensitive", false)); - match.remove("caseSensitive"); - sanitize(match); - this.matches.add(match); - } + rules.add(new Rule(step)); } } - - if(matches.size() != replaces.size()) { - throw new SubstitutionException("Mismatched instructions; Each \"use\" must have a matching \"for\"."); - } } public JSONObject replace(InputStream in) throws IOException, JSONException { @@ -82,18 +63,12 @@ public JSONObject replace(InputStream in) throws IOException, JSONException { } public JSONObject replace(JSONObject base) throws JSONException { - for(int i = 0; i < matches.size(); i++) { - if (find(base, matches.get(i), matchCase.get(i))) { - log.debug("Matched {}JSON substitution rule: for: {}, use: {}", - matchCase.get(i) ? "case-sensitive " : "", - matches.get(i), - replaces.get(i)); - replace(base, replaces.get(i)); + for(Rule rule : rules) { + if (find(base, rule.match, rule.caseSensitive)) { + log.debug("Matched JSON substitution rule: {}", rule); + replace(base, rule.replace); } else { - log.debug("Unable to match {}JSON substitution rule: for: {}, use: {}", - matchCase.get(i) ? "case-sensitive " : "", - matches.get(i), - replaces.get(i)); + log.debug("Unable to match JSON substitution rule: {}", rule); } } return base; @@ -117,21 +92,20 @@ public static void replace(JSONObject base, JSONObject replace) throws JSONExcep throw new SubstitutionException("Replacement JSON is missing \"params\": and is malformed"); } - // Second pass of sanitization before we replace - for(Iterator it = jsonReplace.keys(); it.hasNext();) { - Type type = Type.parse(it.next()); - if(type != null && !type.isReadOnly()) { + if (strict) { + // Second pass of sanitization before we replace + for(Iterator it = jsonReplace.keys(); it.hasNext(); ) { + Type type = Type.parse(it.next()); + if(type == null || type.isReadOnly()) continue; // Good, let's make sure there are no exceptions - if(strict) { - switch(type) { - case DATA: - // Special handling for arrays - JSONArray jsonArray = jsonReplace.optJSONArray(type.getKey()); - removeRestrictedSubkeys(jsonArray, type); - break; - default: - removeRestrictedSubkeys(jsonReplace, type); - } + switch(type) { + case DATA: + // Special handling for arrays + JSONArray jsonArray = jsonReplace.optJSONArray(type.getKey()); + removeRestrictedSubkeys(jsonArray, type); + break; + default: + removeRestrictedSubkeys(jsonReplace, type); } } } @@ -142,27 +116,32 @@ private static void removeRestrictedSubkeys(JSONObject jsonObject, Type type) { if(jsonObject == null) { return; } - for(Map.Entry entry : parlous.entrySet()) { - if (type == entry.getValue()) { - JSONObject toCheck = jsonObject.optJSONObject(type.getKey()); - if(toCheck != null && toCheck.has(entry.getKey())) { - log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), entry.getKey()); - jsonObject.remove(entry.getKey()); - } + + String[] parlousFieldNames = parlous.get(type); + if(parlousFieldNames == null) return; + + for (String parlousFieldName : parlousFieldNames) { + JSONObject toCheck = jsonObject.optJSONObject(type.getKey()); + if (toCheck != null && toCheck.has(parlousFieldName)) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), parlousFieldName); + jsonObject.remove(parlousFieldName); } } } + private static void removeRestrictedSubkeys(JSONArray jsonArray, Type type) { if(jsonArray == null) { return; } - ArrayList toRemove = new ArrayList(); + + ArrayList toRemove = new ArrayList<>(); for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject; if ((jsonObject = jsonArray.optJSONObject(i)) != null) { - for(Map.Entry entry : parlous.entrySet()) { - if (jsonObject.has(entry.getKey()) && type == entry.getValue()) { - log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), entry.getKey()); + String[] parlousFieldNames = parlous.get(type); + for (String parlousFieldName : parlousFieldNames) { + if (jsonObject.has(parlousFieldName)) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), parlousFieldName); toRemove.add(jsonObject); } } @@ -177,10 +156,9 @@ public static void sanitize(JSONObject match) throws JSONException { // "options" ~= "config" Object cache; - JSONObject nested = new JSONObject(); for(Type key : Type.values()) { - // Sanitize alts/aliases + // If any alts/aliases key are used, switch them out for the standard key for(String alt : key.getAlts()) { if ((cache = match.optJSONObject(alt)) != null) { match.put(key.getKey(), cache); @@ -213,7 +191,7 @@ public static void sanitize(JSONObject match) throws JSONException { JSONObject params = (JSONObject)cache; if((cache = params.opt("data")) != null) { if (cache instanceof JSONArray) { - // correct + // If "data" is already an array, skip } else { JSONArray wrapped = new JSONArray(); wrapped.put(cache); @@ -233,23 +211,23 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo JSONObject jsonMatch = (JSONObject)match; JSONObject jsonBase = (JSONObject)base; for(Iterator it = jsonMatch.keys(); it.hasNext(); ) { - Object next = it.next(); - Object newMatch = jsonMatch.get(next.toString()); + String nextKey = it.next().toString(); + Object newMatch = jsonMatch.get(nextKey); // Check if the key exists, recurse if needed - if(jsonBase.has(next.toString())) { - Object newBase = jsonBase.get(next.toString()); + if(jsonBase.has(nextKey)) { + Object newBase = jsonBase.get(nextKey); if(replace && isPrimitive(newMatch)) { // Overwrite value, don't recurse - jsonBase.put(next.toString(), newMatch); + jsonBase.put(nextKey, newMatch); continue; } else if(find(newBase, newMatch, caseSensitive, replace)) { continue; } } else if(replace) { // Key doesn't exist, so we'll merge it in - jsonBase.put(next.toString(), newMatch); + jsonBase.put(nextKey, newMatch); } return false; // wasn't found } @@ -262,7 +240,6 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo if(match instanceof JSONArray) { JSONArray matchArray = (JSONArray)match; JSONArray baseArray = (JSONArray)base; - match: for(int i = 0; i < matchArray.length(); i++) { Object newMatch = matchArray.get(i); for(int j = 0; j < baseArray.length(); j++) { @@ -270,7 +247,7 @@ private static boolean find(Object base, Object match, boolean caseSensitive, bo if(find(newBase, newMatch, caseSensitive, replace)) { found = true; if(!replace) { - continue match; + return true; } } } @@ -341,4 +318,34 @@ public static Substitutions getInstance(boolean forceRefresh) { public static boolean areActive() { return Substitutions.getInstance() != null && enabled; } + + private class Rule { + private boolean caseSensitive; + private JSONObject match, replace; + + Rule(JSONObject json) throws JSONException { + JSONObject replaceJSON = json.optJSONObject("use"); + if(replaceJSON != null) { + sanitize(replaceJSON); + replace = replaceJSON; + } + + JSONObject matchJSON = json.optJSONObject("for"); + if(matchJSON != null) { + caseSensitive = matchJSON.optBoolean("caseSensitive", false); + matchJSON.remove("caseSensitive"); + sanitize(matchJSON); + match = matchJSON; + } + + if(match == null || replace == null) { + throw new SubstitutionException("Mismatched instructions; Each \"use\" must have a matching \"for\"."); + } + } + + @Override + public String toString() { + return caseSensitive ? "case-sensitive " : "" + "for: " + match + ", use: " + replace; + } + } } From 8bc85f5598255368801c249291824ef7f420ad39 Mon Sep 17 00:00:00 2001 From: Tres Finocchiaro Date: Thu, 2 May 2024 01:11:31 -0400 Subject: [PATCH 17/17] Fix log wording --- src/qz/ws/substitutions/Substitutions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qz/ws/substitutions/Substitutions.java b/src/qz/ws/substitutions/Substitutions.java index deb383ea7..e119c48ed 100644 --- a/src/qz/ws/substitutions/Substitutions.java +++ b/src/qz/ws/substitutions/Substitutions.java @@ -65,10 +65,10 @@ public JSONObject replace(InputStream in) throws IOException, JSONException { public JSONObject replace(JSONObject base) throws JSONException { for(Rule rule : rules) { if (find(base, rule.match, rule.caseSensitive)) { - log.debug("Matched JSON substitution rule: {}", rule); + log.debug("Matched {}JSON substitution rule: {}", rule.caseSensitive ? "case-sensitive " : "", rule); replace(base, rule.replace); } else { - log.debug("Unable to match JSON substitution rule: {}", rule); + log.debug("Unable to match {}JSON substitution rule: {}", rule.caseSensitive ? "case-sensitive " : "", rule); } } return base; @@ -345,7 +345,7 @@ private class Rule { @Override public String toString() { - return caseSensitive ? "case-sensitive " : "" + "for: " + match + ", use: " + replace; + return "for: " + match + ", use: " + replace; } } }