From 1023b017727ca871cc866e55d0d56b9eba277dc0 Mon Sep 17 00:00:00 2001 From: Nan Date: Sun, 15 May 2022 18:44:39 -0700 Subject: [PATCH] Fix crash from `$` in external user ID * If a user has a dollar sign ($) in the external user ID, and our code is trying to escape the forward slashes via a string replacement, this will cause a crash as `$` has a non-literal meaning when used in the replacement string. The solution is to call `quoteReplacement` to escape any $ or \ signs. See https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#replaceAll-java.lang.String-java.lang.String- * Also fix the pattern matching for external user ID. Previously in a JSONObject like {"app_id": "abc", "external_user_id": "user1", "timezone": "Europe/London"}, the regex would match `def", "timezone": "Europe/London`, grabbing the forward slash in ANY values that come after the external_user_id. Fix this to match the external user ID value only. --- .../main/java/com/onesignal/JSONUtils.java | 30 +++++++++ .../com/onesignal/OneSignalRestClient.java | 15 +---- .../OneSignalPackagePrivateHelper.java | 4 ++ .../onesignal/MainOneSignalClassRunner.java | 64 +++++++++++++++++++ 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/JSONUtils.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/JSONUtils.java index 238b2f9b77..5f3d554fb4 100644 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/JSONUtils.java +++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/JSONUtils.java @@ -13,6 +13,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.onesignal.UserStateSynchronizer.EXTERNAL_USER_ID; class JSONUtils { @@ -130,6 +134,32 @@ static String toStringNE(JSONArray jsonArray) { return strArray + "]"; } + /** + * Returns the JSONObject as a String with the external user ID unescaped. + * Needed b/c the default JSONObject.toString() escapes (/) with (\/), which customers may not want. + */ + static String toUnescapedEUIDString(JSONObject json) { + String strJsonBody = json.toString(); + + if (json.has(EXTERNAL_USER_ID)) { + // find the value of the external user ID + Pattern eidPattern = Pattern.compile("(?<=\"external_user_id\":\").*?(?=\")"); + Matcher eidMatcher = eidPattern.matcher(strJsonBody); + + if (eidMatcher.find()) { + String matched = eidMatcher.group(0); + if (matched != null) { + String unescapedEID = matched.replace("\\/", "/"); + // backslashes (\) and dollar signs ($) in the replacement string will be treated literally + unescapedEID = eidMatcher.quoteReplacement(unescapedEID); + strJsonBody = eidMatcher.replaceAll(unescapedEID); + } + } + } + + return strJsonBody; + } + static JSONObject getJSONObjectWithoutBlankValues(ImmutableJSONObject jsonObject, String getKey) { if (!jsonObject.has(getKey)) return null; diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java index ec2bcf2ca9..eb113d2eb4 100644 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java +++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalRestClient.java @@ -40,8 +40,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.util.Scanner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; @@ -168,18 +166,7 @@ private static Thread startHTTPConnection(String url, String method, JSONObject } if (jsonBody != null) { - String strJsonBody = jsonBody.toString(); - - Pattern eidPattern = Pattern.compile("(?<=\"external_user_id\":\").*\\\\/.*?(?=\",|\"\\})"); - Matcher eidMatcher = eidPattern.matcher(strJsonBody); - - if (eidMatcher.find()) { - String matched = eidMatcher.group(0); - if (matched != null) { - String unescapedEID = matched.replace("\\/", "/"); - strJsonBody = eidMatcher.replaceAll(unescapedEID); - } - } + String strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody); OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "OneSignalRestClient: " + method + " SEND JSON: " + strJsonBody); diff --git a/OneSignalSDK/unittest/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java b/OneSignalSDK/unittest/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java index 906349b712..c9463c3984 100644 --- a/OneSignalSDK/unittest/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java +++ b/OneSignalSDK/unittest/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java @@ -129,6 +129,10 @@ public static JSONObject bundleAsJSONObject(Bundle bundle) { return NotificationBundleProcessor.bundleAsJSONObject(bundle); } + public static String toUnescapedEUIDString(JSONObject json) { + return JSONUtils.toUnescapedEUIDString(json); + } + public static void OneSignal_handleNotificationOpen(Activity context, final JSONArray data, final String notificationId) { OneSignal.handleNotificationOpen(context, data, notificationId); } diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java index f030586d62..f84c9434b1 100644 --- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java +++ b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java @@ -141,6 +141,7 @@ import static com.onesignal.OneSignalPackagePrivateHelper.OneSignal_setTrackerFactory; import static com.onesignal.OneSignalPackagePrivateHelper.OneSignal_taskQueueWaitingForInit; import static com.onesignal.OneSignalPackagePrivateHelper.OSObservable; +import static com.onesignal.OneSignalPackagePrivateHelper.toUnescapedEUIDString; import static com.onesignal.ShadowOneSignalRestClient.EMAIL_USER_ID; import static com.onesignal.ShadowOneSignalRestClient.PUSH_USER_ID; import static com.onesignal.ShadowOneSignalRestClient.REST_METHOD; @@ -3378,6 +3379,69 @@ public void testOSNotificationOpenResultToJSONObject() throws Exception { assertEquals("collapseId1", firstGroupedNotification.optString("collapseId")); } + // ####### Unit test JSONUtils methods + @Test + public void test_JSONUtils_toUnescapedEUIDString() throws Exception { + // 1. Test when EUID is first in the json, and has ($) and (/), and ($) elsewhere + + // Set up the JSONObject to test with + String jsonStringWithDollarAndSlash = "{" + + "\"external_user_id\":\"$1$/abc/de$f/\"," + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"$Europe/London\"" + + "}"; + JSONObject jsonWithDollarAndSlash = new JSONObject(jsonStringWithDollarAndSlash); + + // The expected string which escapes the "timezone" slash (/) only + String expected_jsonStringWithDollarAndSlash = "{" + + "\"external_user_id\":\"$1$/abc/de$f/\"," + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"$Europe\\/London\"" + + "}"; + + // The actual string result from calling JSONUtils.toUnescapedEUIDString() + String actual_jsonStringWithDollarAndSlash = toUnescapedEUIDString(jsonWithDollarAndSlash); + + // These two strings should be equal + assertEquals(expected_jsonStringWithDollarAndSlash, actual_jsonStringWithDollarAndSlash); + + // 2. Test when EUID is first in the json, and has no dollar nor slash + + String jsonStringWithEUID = "{" + + "\"external_user_id\":\"123abc\"," + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"$Europe/London\"" + + "}"; + JSONObject jsonWithEUID = new JSONObject(jsonStringWithEUID); + + String expected_jsonStringWithEUID = "{" + + "\"external_user_id\":\"123abc\"," + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"$Europe\\/London\"" + + "}"; + + String actual_jsonStringWithEUID = toUnescapedEUIDString(jsonWithEUID); + + assertEquals(expected_jsonStringWithEUID, actual_jsonStringWithEUID); + + // 3. Test when there is no EUID is in the json + + String jsonStringWithoutEUID = "{" + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"Europe/London\"" + + "}"; + JSONObject jsonWithoutEUID = new JSONObject(jsonStringWithoutEUID); + + String expected_jsonStringWithoutEUID = "{" + + "\"app_id\":\"b4f7f966-d8cc-11e4-bed1-df8f05be55ba\"," + + "\"timezone\":\"Europe\\/London\"" + + "}"; + + String actual_jsonStringWithoutEUID = toUnescapedEUIDString(jsonWithoutEUID); + + assertEquals(expected_jsonStringWithoutEUID, actual_jsonStringWithoutEUID); + } + @Test public void testNotificationOpenedProcessorHandlesEmptyIntent() { NotificationOpenedProcessor_processFromContext(blankActivity, new Intent());