diff --git a/.vs/CommunityCommons/v17/.wsuo b/.vs/CommunityCommons/v17/.wsuo new file mode 100644 index 0000000..d52b54e Binary files /dev/null and b/.vs/CommunityCommons/v17/.wsuo differ diff --git a/.vs/CommunityCommons/v17/DocumentLayout.json b/.vs/CommunityCommons/v17/DocumentLayout.json new file mode 100644 index 0000000..90cf834 --- /dev/null +++ b/.vs/CommunityCommons/v17/DocumentLayout.json @@ -0,0 +1,67 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Users\\NowakM1\\source\\repos\\CommunityCommons\\", + "Documents": [], + "DocumentGroupContainers": [ + { + "Orientation": 1, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedHeight": 206, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:128:0:{75188d03-9892-4ae2-abf1-207126247ce5}" + }, + { + "$type": "Bookmark", + "Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" + } + ] + }, + { + "DockedHeight": 206, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{a80febb4-e7e0-4147-b476-21aaf2453969}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{004be353-6879-467c-9d1e-9ac23cdf6d49}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{d78612c7-9962-4b83-95d9-268046dad23a}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{34e76e81-ee4a-11d0-ae2e-00a0c90fffc3}" + } + ] + }, + { + "DockedHeight": 117, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{e1b7d1f8-9b3c-49b1-8f4f-bfc63a88835d}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..6b61141 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..841c6f6 Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/src/CommunityCommons/CommunityCommons.mpr b/src/CommunityCommons/CommunityCommons.mpr index ce6b069..8015bf6 100644 Binary files a/src/CommunityCommons/CommunityCommons.mpr and b/src/CommunityCommons/CommunityCommons.mpr differ diff --git a/src/CommunityCommons/javasource/communitycommons/StringUtils.java b/src/CommunityCommons/javasource/communitycommons/StringUtils.java index b61b7b9..1b0886c 100644 --- a/src/CommunityCommons/javasource/communitycommons/StringUtils.java +++ b/src/CommunityCommons/javasource/communitycommons/StringUtils.java @@ -53,15 +53,11 @@ public class StringUtils { static final String SPECIAL = stringRange('!', '/'); private static final String ALPHANUMERIC = UPPERCASE_ALPHA + LOWERCASE_ALPHA + DIGITS; - static final Map SANITIZER_POLICIES = - Map.ofEntries( + static final Map SANITIZER_POLICIES = Map.ofEntries( new SimpleEntry<>(BLOCKS.name(), Sanitizers.BLOCKS), new SimpleEntry<>(FORMATTING.name(), Sanitizers.FORMATTING), - new SimpleEntry<>(IMAGES.name(), Sanitizers.IMAGES), - new SimpleEntry<>(LINKS.name(), Sanitizers.LINKS), - new SimpleEntry<>(STYLES.name(), Sanitizers.STYLES), - new SimpleEntry<>(TABLES.name(), Sanitizers.TABLES) - ); + new SimpleEntry<>(IMAGES.name(), Sanitizers.IMAGES), new SimpleEntry<>(LINKS.name(), Sanitizers.LINKS), + new SimpleEntry<>(STYLES.name(), Sanitizers.STYLES), new SimpleEntry<>(TABLES.name(), Sanitizers.TABLES)); public static final String HASH_ALGORITHM = "SHA-256"; @@ -93,16 +89,17 @@ public static String hash(String value, int length) throws NoSuchAlgorithmExcept } /** - * The default replaceAll microflow function doesn't support capture variables such as $1, $2 - * etc. so for that reason we do not deprecate this method. + * The default replaceAll microflow function doesn't support capture variables + * such as $1, $2 etc. so for that reason we do not deprecate this method. * * @param haystack The string to replace patterns in * @param needleRegex The regular expression pattern - * @param replacement The string that should come in place of the pattern matches. - * @return The resulting string, where all matches have been replaced by the replacement. + * @param replacement The string that should come in place of the pattern + * matches. + * @return The resulting string, where all matches have been replaced by the + * replacement. */ - public static String regexReplaceAll(String haystack, String needleRegex, - String replacement) { + public static String regexReplaceAll(String haystack, String needleRegex, String replacement) { Pattern pattern = Pattern.compile(needleRegex); Matcher matcher = pattern.matcher(haystack); return matcher.replaceAll(replacement); @@ -126,8 +123,8 @@ public static String randomString(int length) { return randomStringFromCharArray(length, ALPHANUMERIC.toCharArray()); } - public static String substituteTemplate(final IContext context, String template, - final IMendixObject substitute, final boolean HTMLEncode, final String datetimeformat) { + public static String substituteTemplate(final IContext context, String template, final IMendixObject substitute, + final boolean HTMLEncode, final String datetimeformat) { return regexReplaceAll(template, "\\{(@)?([\\w./]+)\\}", (MatchResult match) -> { String value; String path = match.group(2); @@ -144,7 +141,8 @@ public static String substituteTemplate(final IContext context, String template, }); } - public static String regexReplaceAll(String source, String regexString, Function replaceFunction) { + public static String regexReplaceAll(String source, String regexString, + Function replaceFunction) { if (source == null || source.trim().isEmpty()) // avoid NPE's, save CPU { return ""; @@ -214,6 +212,25 @@ public static String base64EncodeFile(IContext context, FileDocument file) throw } } + public static String stringFromFileUtf8Bom(IContext context, FileDocument source) throws IOException { + if (source == null) { + return null; + } + + // Read using normal UTF‑8 + String value = stringFromFile(context, source, StandardCharsets.UTF_8); + if (value == null || value.isEmpty()) { + return value; + } + + // Remove BOM character (U+FEFF) if present at the beginning + if (value.charAt(0) == '\uFEFF') { + return value.substring(1); + } + + return value; + } + public static String stringFromFile(IContext context, FileDocument source) throws IOException { return stringFromFile(context, source, StandardCharsets.UTF_8); } @@ -231,11 +248,25 @@ public static String stringFromInputStream(InputStream inputStream, Charset char return IOUtils.toString(BOMInputStream.builder().setInputStream(inputStream).get(), charset); } + public static void stringToFileUtf8Bom(IContext context, String value, FileDocument destination) + throws IOException { + if (destination == null) { + throw new IllegalArgumentException("Destination file is null"); + } + if (value == null) { + throw new IllegalArgumentException("Value to write is null"); + } + + // Prepend BOM as U+FEFF, then write with standard UTF-8 + stringToFile(context, "\uFEFF" + value, destination, StandardCharsets.UTF_8); + } + public static void stringToFile(IContext context, String value, FileDocument destination) throws IOException { stringToFile(context, value, destination, StandardCharsets.UTF_8); } - public static void stringToFile(IContext context, String value, FileDocument destination, Charset charset) throws IOException { + public static void stringToFile(IContext context, String value, FileDocument destination, Charset charset) + throws IOException { if (destination == null) { throw new IllegalArgumentException("Destination file is null"); } @@ -281,30 +312,32 @@ public void handleEndTag(HTML.Tag tag, int pos) { } /** - * Returns a random strong password containing a specified minimum number of uppercase, digits - * and the exact number of special characters. + * Returns a random strong password containing a specified minimum number of + * uppercase, digits and the exact number of special characters. * * @param minLen Minimum length * @param maxLen Maximum length * @param noOfCAPSAlpha Minimum number of capitals * @param noOfDigits Minimum number of digits * @param noOfSplChars Exact number of special characters - * @deprecated Use the overload randomStrongPassword instead + * @deprecated Use the overload randomStrongPassword instead */ @Deprecated - public static String randomStrongPassword(int minLen, int maxLen, int noOfCAPSAlpha, int noOfDigits, int noOfSplChars) { + public static String randomStrongPassword(int minLen, int maxLen, int noOfCAPSAlpha, int noOfDigits, + int noOfSplChars) { if (minLen > maxLen) { throw new IllegalArgumentException("Min. Length > Max. Length!"); } if ((noOfCAPSAlpha + noOfDigits + noOfSplChars) > minLen) { - throw new IllegalArgumentException("Min. Length should be atleast sum of (CAPS, DIGITS, SPL CHARS) Length!"); + throw new IllegalArgumentException( + "Min. Length should be atleast sum of (CAPS, DIGITS, SPL CHARS) Length!"); } return generateCommonLangPassword(minLen, maxLen, noOfCAPSAlpha, 0, noOfDigits, noOfSplChars); } /** - * Returns a random strong password containing a specified minimum number of uppercase, lowercase, digits - * and the exact number of special characters. + * Returns a random strong password containing a specified minimum number of + * uppercase, lowercase, digits and the exact number of special characters. * * @param minLen Minimum length * @param maxLen Maximum length @@ -313,19 +346,24 @@ public static String randomStrongPassword(int minLen, int maxLen, int noOfCAPSAl * @param noOfDigits Minimum number of digits * @param noOfSplChars Exact number of special characters */ - public static String randomStrongPassword(int minLen, int maxLen, int noOfCAPSAlpha, int noOfLowercaseAlpha, int noOfDigits, int noOfSplChars) { + public static String randomStrongPassword(int minLen, int maxLen, int noOfCAPSAlpha, int noOfLowercaseAlpha, + int noOfDigits, int noOfSplChars) { if (minLen > maxLen) { throw new IllegalArgumentException("Min. Length > Max. Length!"); } if ((noOfCAPSAlpha + noOfLowercaseAlpha + noOfDigits + noOfSplChars) > minLen) { - throw new IllegalArgumentException("Min. Length should be atleast sum of (CAPS, LOWER, DIGITS, SPL CHARS) Length!"); + throw new IllegalArgumentException( + "Min. Length should be atleast sum of (CAPS, LOWER, DIGITS, SPL CHARS) Length!"); } return generateCommonLangPassword(minLen, maxLen, noOfCAPSAlpha, noOfLowercaseAlpha, noOfDigits, noOfSplChars); } // See https://www.baeldung.com/java-generate-secure-password - // Implementation inspired by https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-string-apis (under MIT license) - private static String generateCommonLangPassword(int minLen, int maxLen, int noOfCapsAlpha, int noOfLowercaseAlpha, int noOfDigits, int noOfSplChars) { + // Implementation inspired by + // https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-string-apis + // (under MIT license) + private static String generateCommonLangPassword(int minLen, int maxLen, int noOfCapsAlpha, int noOfLowercaseAlpha, + int noOfDigits, int noOfSplChars) { String upperCaseLetters = randomStringFromCharArray(noOfCapsAlpha, UPPERCASE_ALPHA.toCharArray()); String lowerCaseLetters = randomStringFromCharArray(noOfLowercaseAlpha, LOWERCASE_ALPHA.toCharArray()); String numbers = randomStringFromCharArray(noOfDigits, DIGITS.toCharArray()); @@ -336,29 +374,24 @@ private static String generateCommonLangPassword(int minLen, int maxLen, int noO final int upperBound = maxLen - fixedNumber; String totalChars = randomStringFromCharArray(lowerBound, upperBound, ALPHANUMERIC.toCharArray()); - String combinedChars = upperCaseLetters - .concat(lowerCaseLetters) - .concat(numbers) - .concat(specialChar) - .concat(totalChars); - List pwdChars = combinedChars.chars() - .mapToObj(c -> (char) c) - .collect(Collectors.toList()); + String combinedChars = upperCaseLetters.concat(lowerCaseLetters).concat(numbers).concat(specialChar) + .concat(totalChars); + List pwdChars = combinedChars.chars().mapToObj(c -> (char) c).collect(Collectors.toList()); Collections.shuffle(pwdChars); - String password = pwdChars.stream() - .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) - .toString(); + String password = pwdChars.stream().collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) + .toString(); return password; } /** - * Generate a secure random string using the given array of characters, of which the resulting - * string will be composed of. + * Generate a secure random string using the given array of characters, of which + * the resulting string will be composed of. * * @param count The length of the random string. * @param allowedChars The characters used for constructing the random string. * @return A random string. - * @throws IllegalArgumentException if count is negative or allowedChars is null or empty. + * @throws IllegalArgumentException if count is negative or + * allowedChars is null or empty. */ private static String randomStringFromCharArray(int count, final char[] allowedChars) { if (count == 0) @@ -381,33 +414,44 @@ private static String randomStringFromCharArray(int count, final char[] allowedC } /** - * Generate a random string with a random length between minLengthBound and maxLengthBound (inclusive), + * Generate a random string with a random length between + * minLengthBound and maxLengthBound (inclusive), * using the given set of allowed characters. * * @param minLengthBound The lower bound for the random length of the string. * @param maxLengthBound The upper bound for the random length of the string. - * @param allowedChars An array of characters of which the resulting string will be made up of. - * @return A random string with a length between minLengthBound and maxLengthBound. - * @throws IllegalArgumentException if minLengthBound is larger than maxLengthBound. + * @param allowedChars An array of characters of which the resulting string + * will be made up of. + * @return A random string with a length between minLengthBound and + * maxLengthBound. + * @throws IllegalArgumentException if minLengthBound is larger + * than maxLengthBound. */ private static String randomStringFromCharArray(int minLengthBound, int maxLengthBound, final char[] allowedChars) { if (minLengthBound == maxLengthBound) return randomStringFromCharArray(minLengthBound, allowedChars); if (minLengthBound > maxLengthBound) - throw new IllegalArgumentException("The minimum bound (" + minLengthBound + ") was larger than the maximum bound (" + maxLengthBound + "."); - final int randomLength = minLengthBound + RANDOM.nextInt(maxLengthBound - minLengthBound + 1); // add one to make the range inclusive. + throw new IllegalArgumentException("The minimum bound (" + minLengthBound + + ") was larger than the maximum bound (" + maxLengthBound + "."); + final int randomLength = minLengthBound + RANDOM.nextInt(maxLengthBound - minLengthBound + 1); // add one to + // make the + // range + // inclusive. return randomStringFromCharArray(randomLength, allowedChars); } /** - * Produces a 'range' string starting from the begin character up to - * the end character (inclusive range). For example, for the range (a-z), - * this method will generate the lowercase alphabet. + * Produces a 'range' string starting from the begin character up + * to the end character (inclusive range). For example, for the + * range (a-z), this method will generate the lowercase alphabet. * * @param begin The starting point of the string. * @param end The ending point of the string. - * @return A string from begin to end (inclusive range). - * @throws IllegalArgumentException if the begin character has a higher code point than the end character. + * @return A string from begin to end (inclusive + * range). + * @throws IllegalArgumentException if the begin character has a + * higher code point than the end + * character. */ private static String stringRange(char begin, char end) { if (begin > end) { @@ -420,7 +464,8 @@ private static String stringRange(char begin, char end) { return builder.toString(); } - private static byte[] generateHmacSha256Bytes(String key, String valueToEncrypt) throws UnsupportedEncodingException, IllegalStateException, InvalidKeyException, NoSuchAlgorithmException { + private static byte[] generateHmacSha256Bytes(String key, String valueToEncrypt) + throws UnsupportedEncodingException, IllegalStateException, InvalidKeyException, NoSuchAlgorithmException { SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKey); @@ -438,7 +483,8 @@ public static String generateHmacSha256(String key, String valueToEncrypt) { result.append(String.format("%02x", b)); } return result.toString(); - } catch (UnsupportedEncodingException | IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) { + } catch (UnsupportedEncodingException | IllegalStateException | InvalidKeyException + | NoSuchAlgorithmException e) { throw new RuntimeException("CommunityCommons::generateHmacSha256::Unable to encode: " + e.getMessage(), e); } } @@ -446,18 +492,19 @@ public static String generateHmacSha256(String key, String valueToEncrypt) { public static String generateHmacSha256Hash(String key, String valueToEncrypt) { try { return Base64.getEncoder().encodeToString(generateHmacSha256Bytes(key, valueToEncrypt)); - } catch (UnsupportedEncodingException | IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) { - throw new RuntimeException("CommunityCommons::generateHmacSha256Hash::Unable to encode: " + e.getMessage(), e); + } catch (UnsupportedEncodingException | IllegalStateException | InvalidKeyException + | NoSuchAlgorithmException e) { + throw new RuntimeException("CommunityCommons::generateHmacSha256Hash::Unable to encode: " + e.getMessage(), + e); } } public static String escapeHTML(String input) { - return input.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'");// notice this one: for xml "'" would be "'" (http://blogs.msdn.com/b/kirillosenkov/archive/2010/03/19/apos-is-in-xml-in-html-use-39.aspx) - // OWASP also advises to escape "/" but give no convincing reason why: https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet + return input.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) + .replace("'", "'");// notice this one: for xml "'" would be "'" + // (http://blogs.msdn.com/b/kirillosenkov/archive/2010/03/19/apos-is-in-xml-in-html-use-39.aspx) + // OWASP also advises to escape "/" but give no convincing reason why: + // https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet } public static String regexQuote(String unquotedLiteral) { diff --git a/src/CommunityCommons/javasource/communitycommons/actions/StringFromFile.java b/src/CommunityCommons/javasource/communitycommons/actions/StringFromFile.java index 6ead981..b1fa3e4 100644 --- a/src/CommunityCommons/javasource/communitycommons/actions/StringFromFile.java +++ b/src/CommunityCommons/javasource/communitycommons/actions/StringFromFile.java @@ -17,22 +17,17 @@ import com.mendix.systemwideinterfaces.core.UserAction; /** - * Reads the contents form the provided file document, using the specified encoding, and returns it as string. + * Reads the contents form the provided file document, using the specified + * encoding, and returns it as string. */ -public class StringFromFile extends UserAction -{ +public class StringFromFile extends UserAction { /** @deprecated use source.getMendixObject() instead. */ @java.lang.Deprecated(forRemoval = true) private final IMendixObject __source; private final system.proxies.FileDocument source; private final communitycommons.proxies.StandardEncodings encoding; - public StringFromFile( - IContext context, - IMendixObject _source, - java.lang.String _encoding - ) - { + public StringFromFile(IContext context, IMendixObject _source, java.lang.String _encoding) { super(context); this.__source = _source; this.source = _source == null ? null : system.proxies.FileDocument.initialize(getContext(), _source); @@ -40,9 +35,13 @@ public StringFromFile( } @java.lang.Override - public java.lang.String executeAction() throws Exception - { + public java.lang.String executeAction() throws Exception { // BEGIN USER CODE + + if (this.encoding == communitycommons.proxies.StandardEncodings.UTF_8_BOM) { + return StringUtils.stringFromFileUtf8Bom(getContext(), source); + } + Charset charset = StandardCharsets.UTF_8; if (this.encoding != null) charset = Charset.forName(this.encoding.name().replace('_', '-')); @@ -52,11 +51,11 @@ public java.lang.String executeAction() throws Exception /** * Returns a string representation of this action + * * @return a string representation of this action */ @java.lang.Override - public java.lang.String toString() - { + public java.lang.String toString() { return "StringFromFile"; } diff --git a/src/CommunityCommons/javasource/communitycommons/actions/StringToFile.java b/src/CommunityCommons/javasource/communitycommons/actions/StringToFile.java index ec76e71..a5ad1af 100644 --- a/src/CommunityCommons/javasource/communitycommons/actions/StringToFile.java +++ b/src/CommunityCommons/javasource/communitycommons/actions/StringToFile.java @@ -47,6 +47,12 @@ public StringToFile( public java.lang.Boolean executeAction() throws Exception { // BEGIN USER CODE + + if (this.encoding == communitycommons.proxies.StandardEncodings.UTF_8_BOM) { + StringUtils.stringToFileUtf8Bom(getContext(), value, destination); + return true; + } + Charset charset = StandardCharsets.UTF_8; if (this.encoding != null) charset = Charset.forName(this.encoding.name().replace('_', '-'));