From 8b8eb84faead4c773cb12e50cbaa7853a2c2121d Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 23 Jan 2024 19:36:43 -0600 Subject: [PATCH] Moved the CSRF token client over into main commons code (#5471) --- app/build.gradle | 4 +- .../nrw/commons/actions/PageEditClient.kt | 2 +- .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../commons/auth}/csrf/CsrfTokenClient.java | 2 +- .../free/nrw/commons/di/NetworkingModule.java | 2 +- .../notification/NotificationClient.kt | 2 +- .../free/nrw/commons/upload/UploadClient.java | 2 +- .../nrw/commons/wikidata/WikiBaseClient.java | 2 +- .../free/nrw/commons/MockWebServerTest.java | 111 ++++++++++++++++++ .../fr/free/nrw/commons/TestFileUtil.java | 37 ++++++ .../fr/free/nrw/commons/TestWebServer.java | 56 +++++++++ .../nrw/commons/actions/PageEditClientTest.kt | 3 +- .../nrw/commons/actions/ThanksClientTest.kt | 5 +- .../auth}/csrf/CsrfTokenClientTest.java | 37 +++--- .../notification/NotificationClientTest.kt | 2 +- .../wikidata/WikiBaseClientUnitTest.kt | 2 +- app/src/test/res/raw/api_error.json | 10 ++ app/src/test/res/raw/csrf_token.json | 8 ++ 18 files changed, 252 insertions(+), 37 deletions(-) rename {data-client/src/main/java/org/wikipedia => app/src/main/java/fr/free/nrw/commons/auth}/csrf/CsrfTokenClient.java (99%) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java rename {data-client/src/test/java/org/wikipedia => app/src/test/kotlin/fr/free/nrw/commons/auth}/csrf/CsrfTokenClientTest.java (75%) create mode 100644 app/src/test/res/raw/api_error.json create mode 100644 app/src/test/res/raw/csrf_token.json diff --git a/app/build.gradle b/app/build.gradle index c309a52b2..94a28afe8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,8 @@ dependencies { // Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies //force = true //API 19 support } + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation "com.squareup.retrofit2:converter-gson:2.8.1" implementation 'com.squareup.okio:okio:2.2.2' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.3' @@ -99,6 +101,7 @@ dependencies { testImplementation 'com.facebook.soloader:soloader:0.10.5' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" debugImplementation("androidx.fragment:fragment-testing:1.6.2") + testImplementation "commons-io:commons-io:2.6" // Android testing androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04' @@ -134,7 +137,6 @@ dependencies { implementation "androidx.room:room-rxjava2:$ROOM_VERSION" kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor - implementation 'com.squareup.retrofit2:retrofit:2.8.1' testImplementation "androidx.arch.core:core-testing:2.1.0" // Pref diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt index cf7b9865c..88c4ff9a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt @@ -2,7 +2,7 @@ package fr.free.nrw.commons.actions import io.reactivex.Observable import io.reactivex.Single -import org.wikipedia.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient /** * This class acts as a Client to facilitate wiki page editing diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index 7533fbc47..3e798176a 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF import io.reactivex.Observable -import org.wikipedia.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton diff --git a/data-client/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java similarity index 99% rename from data-client/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java rename to app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java index 78b9fa8c2..3d631f8ea 100644 --- a/data-client/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java @@ -1,4 +1,4 @@ -package org.wikipedia.csrf; +package fr.free.nrw.commons.auth.csrf; import android.text.TextUtils; diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index f9a9e0491..07195cfa5 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -35,7 +35,7 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor.Level; -import org.wikipedia.csrf.CsrfTokenClient; +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.WikiSite; diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt index a4cba82b5..fb4963dcd 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt @@ -5,7 +5,7 @@ import fr.free.nrw.commons.notification.models.Notification import fr.free.nrw.commons.notification.models.NotificationType import io.reactivex.Observable import io.reactivex.Single -import org.wikipedia.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.util.DateUtil import javax.inject.Inject diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java index 0577ee39d..51560c312 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.java @@ -26,7 +26,7 @@ import javax.inject.Singleton; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; -import org.wikipedia.csrf.CsrfTokenClient; +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; import org.wikipedia.dataclient.mwapi.MwException; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java index a2233a004..f812228bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikiBaseClient.java @@ -9,7 +9,7 @@ import io.reactivex.Observable; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; -import org.wikipedia.csrf.CsrfTokenClient; +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; import org.wikipedia.dataclient.mwapi.MwPostResponse; import timber.log.Timber; diff --git a/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java new file mode 100644 index 000000000..9ef2214b8 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java @@ -0,0 +1,111 @@ +package fr.free.nrw.commons; + +import androidx.annotation.NonNull; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import okhttp3.Dispatcher; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.wikipedia.AppAdapter; +import org.wikipedia.dataclient.Service; +import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.json.GsonUtil; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +@RunWith(RobolectricTestRunner.class) +public abstract class MockWebServerTest { + private OkHttpClient okHttpClient; + private final TestWebServer server = new TestWebServer(); + + @Before public void setUp() throws Throwable { + AppAdapter.set(new TestAppAdapter()); + OkHttpClient.Builder builder = AppAdapter.get().getOkHttpClient(new WikiSite(Service.WIKIPEDIA_URL)).newBuilder(); + okHttpClient = builder.dispatcher(new Dispatcher(new ImmediateExecutorService())).build(); + server.setUp(); + } + + @After public void tearDown() throws Throwable { + server.tearDown(); + } + + @NonNull protected TestWebServer server() { + return server; + } + + protected void enqueueFromFile(@NonNull String filename) throws Throwable { + String json = TestFileUtil.readRawFile(filename); + server.enqueue(json); + } + + protected void enqueue404() { + final int code = 404; + server.enqueue(new MockResponse().setResponseCode(code).setBody("Not Found")); + } + + protected void enqueueMalformed() { + server.enqueue("(╯°□°)╯︵ ┻━┻"); + } + + protected void enqueueEmptyJson() { + server.enqueue(new MockResponse().setBody("{}")); + } + + @NonNull protected OkHttpClient okHttpClient() { + return okHttpClient; + } + + @NonNull protected T service(Class clazz) { + return service(clazz, server().getUrl()); + } + + @NonNull protected T service(Class clazz, @NonNull String url) { + return new Retrofit.Builder() + .baseUrl(url) + .callbackExecutor(new ImmediateExecutor()) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) + .build() + .create(clazz); + } + + public final class ImmediateExecutorService extends AbstractExecutorService { + @Override public void shutdown() { + throw new UnsupportedOperationException(); + } + + @NonNull @Override public List shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override public boolean awaitTermination(long l, @NonNull TimeUnit timeUnit) + throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override public void execute(@NonNull Runnable runnable) { + runnable.run(); + } + } + + public class ImmediateExecutor implements Executor { + @Override + public void execute(@NonNull Runnable runnable) { + runnable.run(); + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java b/app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java new file mode 100644 index 000000000..b87f078f4 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java @@ -0,0 +1,37 @@ +package fr.free.nrw.commons; + +import android.annotation.TargetApi; +import androidx.annotation.NonNull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +public final class TestFileUtil { + private static final String RAW_DIR = "src/test/res/raw/"; + + public static File getRawFile(@NonNull String rawFileName) { + return new File(RAW_DIR + rawFileName); + } + + public static String readRawFile(String basename) throws IOException { + return readFile(getRawFile(basename)); + } + + @TargetApi(19) + private static String readFile(File file) throws IOException { + return FileUtils.readFileToString(file, StandardCharsets.UTF_8); + } + + @TargetApi(19) + public static String readStream(InputStream stream) throws IOException { + StringWriter writer = new StringWriter(); + IOUtils.copy(stream, writer, StandardCharsets.UTF_8); + return writer.toString(); + } + + private TestFileUtil() { } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java b/app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java new file mode 100644 index 000000000..93f79ddce --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java @@ -0,0 +1,56 @@ +package fr.free.nrw.commons; + +import androidx.annotation.NonNull; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +public class TestWebServer { + public static final int TIMEOUT_DURATION = 5; + public static final TimeUnit TIMEOUT_UNIT = TimeUnit.SECONDS; + + private final MockWebServer server; + + public TestWebServer() { + server = new MockWebServer(); + } + + public void setUp() throws IOException { + server.start(); + } + + public void tearDown() throws IOException { + server.shutdown(); + } + + public String getUrl() { + return getUrl(""); + } + + public String getUrl(String path) { + return server.url(path).url().toString(); + } + + public int getRequestCount() { + return server.getRequestCount(); + } + + public void enqueue(@NonNull String body) { + enqueue(new MockResponse().setBody(body)); + } + + public void enqueue(MockResponse response) { + server.enqueue(response); + } + + @NonNull public RecordedRequest takeRequest() throws InterruptedException { + RecordedRequest req = server.takeRequest(TIMEOUT_DURATION, + TIMEOUT_UNIT); + if (req == null) { + throw new InterruptedException("Timeout elapsed."); + } + return req; + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt index 97a158e4d..ac1534eb2 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt @@ -9,8 +9,7 @@ import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import org.wikipedia.csrf.CsrfTokenClient -import org.wikipedia.dataclient.Service +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import org.wikipedia.edit.Edit class PageEditClientTest { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt index 7b6a84217..0fa4535b4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt @@ -12,12 +12,9 @@ import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner import org.robolectric.RobolectricTestRunner -import org.wikipedia.csrf.CsrfTokenClient -import org.wikipedia.dataclient.Service +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient @RunWith(RobolectricTestRunner::class) @PrepareForTest(CommonsApplication::class) diff --git a/data-client/src/test/java/org/wikipedia/csrf/CsrfTokenClientTest.java b/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java similarity index 75% rename from data-client/src/test/java/org/wikipedia/csrf/CsrfTokenClientTest.java rename to app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java index addc6ef69..7543086ae 100644 --- a/data-client/src/test/java/org/wikipedia/csrf/CsrfTokenClientTest.java +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java @@ -1,27 +1,22 @@ -package org.wikipedia.csrf; +package fr.free.nrw.commons.auth.csrf; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import androidx.annotation.NonNull; - import com.google.gson.stream.MalformedJsonException; - +import fr.free.nrw.commons.MockWebServerTest; +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient.Callback; import org.junit.Test; -import org.wikipedia.csrf.CsrfTokenClient.Callback; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.mwapi.MwException; import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.dataclient.okhttp.HttpStatusException; -import org.wikipedia.test.MockWebServerTest; - import retrofit2.Call; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - public class CsrfTokenClientTest extends MockWebServerTest { private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org"); @NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI); @@ -30,7 +25,7 @@ public class CsrfTokenClientTest extends MockWebServerTest { String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"; enqueueFromFile("csrf_token.json"); - Callback cb = mock(Callback.class); + Callback cb = Mockito.mock(Callback.class); request(cb); server().takeRequest(); @@ -40,7 +35,7 @@ public class CsrfTokenClientTest extends MockWebServerTest { @Test public void testRequestResponseApiError() throws Throwable { enqueueFromFile("api_error.json"); - Callback cb = mock(Callback.class); + Callback cb = Mockito.mock(Callback.class); request(cb); server().takeRequest(); @@ -50,7 +45,7 @@ public class CsrfTokenClientTest extends MockWebServerTest { @Test public void testRequestResponseFailure() throws Throwable { enqueue404(); - Callback cb = mock(Callback.class); + Callback cb = Mockito.mock(Callback.class); request(cb); server().takeRequest(); @@ -60,7 +55,7 @@ public class CsrfTokenClientTest extends MockWebServerTest { @Test public void testRequestResponseMalformed() throws Throwable { enqueueMalformed(); - Callback cb = mock(Callback.class); + Callback cb = Mockito.mock(Callback.class); request(cb); server().takeRequest(); @@ -69,16 +64,16 @@ public class CsrfTokenClientTest extends MockWebServerTest { private void assertCallbackSuccess(@NonNull Callback cb, @NonNull String expected) { - verify(cb).success(eq(expected)); + verify(cb).success(ArgumentMatchers.eq(expected)); //noinspection unchecked - verify(cb, never()).failure(any(Throwable.class)); + verify(cb, never()).failure(ArgumentMatchers.any(Throwable.class)); } private void assertCallbackFailure(@NonNull Callback cb, @NonNull Class throwable) { //noinspection unchecked - verify(cb, never()).success(any(String.class)); - verify(cb).failure(isA(throwable)); + verify(cb, never()).success(ArgumentMatchers.any(String.class)); + verify(cb).failure(ArgumentMatchers.isA(throwable)); } private Call request(@NonNull Callback cb) { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt index d1cd5a666..31c345ab7 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt @@ -17,7 +17,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import org.wikipedia.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.json.GsonUtil diff --git a/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt index 6103b82db..03ed2ebc5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt @@ -6,7 +6,7 @@ import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import org.wikipedia.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient class WikiBaseClientUnitTest { diff --git a/app/src/test/res/raw/api_error.json b/app/src/test/res/raw/api_error.json new file mode 100644 index 000000000..8bc56bb7c --- /dev/null +++ b/app/src/test/res/raw/api_error.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "code": "unknown_action", + "text": "Unrecognized value for parameter \"action\": oscillate." + } + ], + "docref": "See https://en.wikipedia.org/w/api.php for API usage.", + "servedby": "mw1286" +} \ No newline at end of file diff --git a/app/src/test/res/raw/csrf_token.json b/app/src/test/res/raw/csrf_token.json new file mode 100644 index 000000000..2b27097e2 --- /dev/null +++ b/app/src/test/res/raw/csrf_token.json @@ -0,0 +1,8 @@ +{ + "batchcomplete": true, + "query": { + "tokens": { + "csrftoken": "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\" + } + } +} \ No newline at end of file