From 8bf77817d2413020cc123c2707eadf9e7233ee8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 22 Mar 2023 23:15:45 +0100 Subject: [PATCH 01/10] [UI] Fix writing files on Android 13 and newer. --- .../main/java/pl/szczodrzynski/edziennik/App.kt | 1 + .../timetable/GenerateBlockTimetableDialog.kt | 10 ++++------ .../edziennik/ui/views/AttachmentsView.kt | 9 +++++++++ .../pl/szczodrzynski/edziennik/utils/Utils.java | 17 ++++++++++++----- app/src/main/res/values-en/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index cf688b03..74e23d88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -235,6 +235,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } Signing.getCert(this) + Utils.initializeStorageDir(this) launch { withContext(Dispatchers.Default) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt index 701ed9ed..66401d31 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt @@ -109,12 +109,10 @@ class GenerateBlockTimetableDialog( .show() dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick { - app.permissionManager.requestStoragePermission(activity, permissionMessage = R.string.permissions_generate_timetable) { - when (b.weekSelectionRadioGroup.checkedRadioButtonId) { - R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd) - R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd) - R.id.forSelectedWeekRadio -> selectDate() - } + when (b.weekSelectionRadioGroup.checkedRadioButtonId) { + R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd) + R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd) + R.id.forSelectedWeekRadio -> selectDate() } } }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt index 7e2bc5af..81240d62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.ui.views import android.content.Context +import android.os.Build import android.os.Bundle import android.util.AttributeSet import androidx.appcompat.app.AppCompatActivity @@ -50,6 +51,10 @@ class AttachmentsView @JvmOverloads constructor( val attachmentSizes = arguments.getLongArray("attachmentSizes") val adapter = AttachmentAdapter(context, onAttachmentClick = { item -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + downloadAttachment(item) + return@AttachmentAdapter + } app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) { downloadAttachment(item) } @@ -57,6 +62,10 @@ class AttachmentsView @JvmOverloads constructor( val popupMenu = PopupMenu(chip.context, chip) popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again) popupMenu.setOnMenuItemClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + downloadAttachment(item) + return@setOnMenuItemClickListener true + } app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) { downloadAttachment(item, forceDownload = true) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java index d4ffae05..f039952d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java @@ -774,14 +774,21 @@ public class Utils { private static File storageDir = null; public static File getStorageDir() { - if (storageDir != null) - return storageDir; - storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - storageDir = new File(storageDir, "Szkolny.eu"); - storageDir.mkdirs(); return storageDir; } + public static void initializeStorageDir(Context context) { + if (storageDir != null) + return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + storageDir = context.getExternalFilesDir(null); + } else { + storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + storageDir = new File(storageDir, "Szkolny.eu"); + } + storageDir.mkdirs(); + } + public static void writeStringToFile(File file, String data) throws IOException { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file)); outputStreamWriter.write(data); diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 50043dac..0be15ebe 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1353,7 +1353,6 @@ Child %s does not have a profile on this account in the current school year. Probably this profile has been deleted or the student no longer attends this class.\n\nTo go to the current profile, select a student from the list or log in to their account with the Add student button. A reference to a remote repository was not found. Make sure you are using the official repository fork and verify your Gradle configuration. "Enter the data you use to log in to the MobiDziennik website. As the server address, you can enter the address of the website where you have MobiDziennik. " - In order to be able to save the generated timetable, you must grant access rights to the device\'s memory.\n\nClick OK to grant permissions. (Child) (Parent) Teachers diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e75273d8..d8b80368 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1416,7 +1416,6 @@ Sprawdź kod Brak dostępu do API Data kompilacji - Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia. przeczytanie Polityki prywatności i akceptujesz jej postanowienia.

Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]>
Szkolny.eu v%s\n%s Ustawienia terminarza From 31b569b02e78f8120b2d7858d8f0e0aa8fbcc5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 22 Mar 2023 23:16:28 +0100 Subject: [PATCH 02/10] [4.13.5] Update build.gradle, signing and changelog. --- app/src/main/assets/pl-changelog.html | 11 +++-------- app/src/main/cpp/szkolny-signing.cpp | 2 +- .../edziennik/data/api/szkolny/interceptor/Signing.kt | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-en/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- build.gradle | 4 ++-- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index dc0296b2..d9dbc980 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,13 +1,8 @@ -

Wersja 4.13.4, 2022-12-26

+

Wersja 4.13.5, 2023-03-22



Dzięki za korzystanie ze Szkolnego!
-© [Kuba Szczodrzyński](@kuba2k2) 2022 +© [Kuba Szczodrzyński](@kuba2k2) 2023 diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index fdf05a94..e16b965b 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x4b, 0x43, 0x7e, 0xa2, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x92, 0xc2, 0xe7, 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 1fe8b48b..3304fa2d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MD4BikzMWC===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MD3uL2uE3E===.$param2".sha256() } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b86e1309..24486d1e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -847,7 +847,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński, September 2018 - 2022 + © Kuba Szczodrzyński, September 2018 - 2023 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 0be15ebe..ff2c1c3a 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -849,7 +849,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński, September 2018 – 2022 + © Kuba Szczodrzyński, September 2018 – 2023 Click to check for updates Update Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8b80368..1e8e44d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -916,7 +916,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński, wrzesień 2018 - 2022 + © Kuba Szczodrzyński, wrzesień 2018 - 2023 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja diff --git a/build.gradle b/build.gradle index 3d437bdc..52abf366 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.6.10' release = [ - versionName: "4.13.4", - versionCode: 4130499 + versionName: "4.13.5", + versionCode: 4130599 ] setup = [ From beff1b646022476813ab72697c9a10db46cc4b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 24 Mar 2023 10:56:35 +0100 Subject: [PATCH 03/10] [App] Fix cookie persistence. --- .../interceptor/SignatureInterceptor.kt | 7 +-- .../edziennik/ext/TextExtensions.kt | 6 +++ .../edziennik/network/cookie/DumbCookie.kt | 17 +++++++ .../edziennik/network/cookie/DumbCookieJar.kt | 28 ++++++++--- .../edziennik/ui/debug/LabPageFragment.kt | 47 +++++++++++-------- 5 files changed, 75 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt index 710bcb74..275a1909 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt @@ -29,11 +29,12 @@ class SignatureInterceptor(val app: App) : Interceptor { return chain.proceed( request.newBuilder() .header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY) - .header("X-AppVersion", BuildConfig.VERSION_CODE.toString()) - .header("X-Timestamp", timestamp.toString()) - .header("X-Signature", sign(timestamp, body, url)) .header("X-AppBuild", BuildConfig.BUILD_TYPE) .header("X-AppFlavor", BuildConfig.FLAVOR) + .header("X-AppVersion", BuildConfig.VERSION_CODE.toString()) + .header("X-DeviceId", app.deviceId) + .header("X-Signature", sign(timestamp, body, url)) + .header("X-Timestamp", timestamp.toString()) .build()) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index adb6fe55..d4149320 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -15,6 +15,7 @@ import android.text.style.CharacterStyle import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.StyleSpan +import android.text.style.UnderlineSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder @@ -160,6 +161,11 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } +fun CharSequence?.asUnderlineSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} fun CharSequence.asSpannable( vararg spans: CharacterStyle, substring: CharSequence? = null, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt index 75b60309..53665332 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt @@ -5,8 +5,20 @@ package pl.szczodrzynski.edziennik.network.cookie import okhttp3.Cookie +import okhttp3.HttpUrl class DumbCookie(var cookie: Cookie) { + companion object { + fun deserialize(key: String, value: String): DumbCookie? { + val (domain, _) = key.split('|', limit = 2) + val url = HttpUrl.Builder() + .scheme("https") + .host(domain) + .build() + val cookie = Cookie.parse(url, value) ?: return null + return DumbCookie(cookie) + } + } constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this( Cookie.Builder() @@ -45,4 +57,9 @@ class DumbCookie(var cookie: Cookie) { hash = 31 * hash + cookie.domain().hashCode() return hash } + + fun serialize(): Pair { + val key = cookie.domain() + "|" + cookie.name() + return key to cookie.toString() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt index 39cec6eb..3bb9e0e5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.network.cookie import android.content.Context +import androidx.core.content.edit import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl @@ -26,22 +27,31 @@ class DumbCookieJar( ) : CookieJar { private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE) - val sessionCookies = mutableSetOf() - private val savedCookies = mutableSetOf() + private val sessionCookies = mutableSetOf() + + init { + prefs.all.forEach { (key, value) -> + if (value !is String) + return@forEach + sessionCookies.add(DumbCookie.deserialize(key, value) ?: return@forEach) + } + } + private fun save(dc: DumbCookie) { sessionCookies.remove(dc) sessionCookies.add(dc) if (dc.cookie.persistent() || persistAll) { - savedCookies.remove(dc) - savedCookies.add(dc) + prefs.edit { + val (key, value) = dc.serialize() + putString(key, value) + } } } private fun delete(vararg toRemove: DumbCookie) { sessionCookies.removeAll(toRemove) - savedCookies.removeAll(toRemove) } - override fun saveFromResponse(url: HttpUrl?, cookies: List) { + override fun saveFromResponse(url: HttpUrl, cookies: MutableList) { for (cookie in cookies) { val dc = DumbCookie(cookie) save(dc) @@ -54,6 +64,10 @@ class DumbCookieJar( }.map { it.cookie } } + fun getAllDomains(): List { + return sessionCookies.map { it.cookie } + } + fun get(domain: String, name: String): String? { return sessionCookies.firstOrNull { it.domainMatches(domain) && it.cookie.name() == name @@ -84,7 +98,7 @@ class DumbCookieJar( fun getAll(domain: String): Map { return sessionCookies.filter { it.domainMatches(domain) - }.map { it.cookie.name() to it.cookie.value() }.toMap() + }.associate { it.cookie.name() to it.cookie.value() } } fun remove(domain: String, name: String) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt index 792811eb..209d8e82 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt @@ -181,26 +181,33 @@ class LabPageFragment : LazyFragment(), CoroutineScope { val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) startCoroutineTimer(500L, 300L) { - val text = app.cookieJar.sessionCookies - .map { it.cookie } - .sortedBy { it.domain() } - .groupBy { it.domain() } - .map { - listOf( - it.key.asBoldSpannable(), - ":\n", - it.value - .sortedBy { it.name() } - .map { - listOf( - " ", - it.name(), - "=", - it.value().decode().take(40).asItalicSpannable().asColoredSpannable(colorSecondary) - ).concat("") - }.concat("\n") - ).concat("") - }.concat("\n\n") + val text = app.cookieJar.getAllDomains() + .sortedBy { it.domain() } + .groupBy { it.domain() } + .map { pair -> + listOf( + pair.key.asBoldSpannable(), + ":\n", + pair.value + .sortedBy { it.name() } + .map { cookie -> + listOf( + " ", + if (cookie.persistent()) + cookie.name() + .asUnderlineSpannable() + else + cookie.name(), + "=", + cookie.value() + .decode() + .take(40) + .asItalicSpannable() + .asColoredSpannable(colorSecondary), + ).concat("") + }.concat("\n") + ).concat("") + }.concat("\n\n") b.cookies.text = text } From 8177d4aa2d4150b43e1fb0f4b0973380344c0d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 24 Mar 2023 11:13:00 +0100 Subject: [PATCH 04/10] [Widgets] Fix pending intents mutability. Hide timetable sync button. --- .../szczodrzynski/edziennik/ext/MiscExtensions.kt | 6 ++++++ .../notifications/WidgetNotificationsProvider.kt | 7 ++++--- .../widgets/timetable/WidgetTimetableProvider.kt | 15 ++++----------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt index e53950f9..cd2d8a1f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt @@ -73,6 +73,12 @@ fun pendingIntentFlag(): Int { return 0 } +fun pendingIntentMutable(): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + return PendingIntent.FLAG_MUTABLE + return 0 +} + fun Int?.takeValue() = if (this == -1) null else this fun Int?.takePositive() = if (this == -1 || this == 0) null else this diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt index b4af3f68..a082c3da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt @@ -23,6 +23,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.ext.Bundle import pl.szczodrzynski.edziennik.ext.getJsonObject import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.pendingIntentMutable import pl.szczodrzynski.edziennik.ext.putExtras import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget @@ -50,7 +51,7 @@ class WidgetNotificationsProvider : AppWidgetProvider() { val syncIntent = SzkolnyReceiver.getIntent(context, Bundle( "task" to "SyncRequest" )) - val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentFlag()) + val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent) views.setImageViewBitmap( @@ -71,13 +72,13 @@ class WidgetNotificationsProvider : AppWidgetProvider() { val itemIntent = Intent(context, MainActivity::class.java) itemIntent.action = Intent.ACTION_MAIN - val itemPendingIntent = PendingIntent.getActivity(context, 0, itemIntent, pendingIntentFlag()) + val itemPendingIntent = PendingIntent.getActivity(context, appWidgetId, itemIntent, pendingIntentMutable()) views.setPendingIntentTemplate(R.id.widgetNotificationsListView, itemPendingIntent) val headerIntent = Intent(context, MainActivity::class.java) headerIntent.action = Intent.ACTION_MAIN headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS) - val headerPendingIntent = PendingIntent.getActivity(context, 0, headerIntent, pendingIntentFlag()) + val headerPendingIntent = PendingIntent.getActivity(context, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt index 10df27c3..a0be777c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt @@ -34,6 +34,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSON import pl.szczodrzynski.edziennik.ext.filterOutArchived import pl.szczodrzynski.edziennik.ext.getJsonObject import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.pendingIntentMutable import pl.szczodrzynski.edziennik.ext.putExtras import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity @@ -119,7 +120,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag()) views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, refreshPendingIntent) - views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA)) + views.setViewVisibility(R.id.widgetTimetableSync, View.GONE) views.setImageViewBitmap( R.id.widgetTimetableRefresh, @@ -129,14 +130,6 @@ class WidgetTimetableProvider : AppWidgetProvider() { }.toBitmap() ) - views.setImageViewBitmap( - R.id.widgetTimetableSync, - IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline).apply { - colorInt = Color.WHITE - sizeDp = if (config.bigStyle) 28 else 20 - }.toBitmap() - ) - prepareAppWidget(app, appWidgetId, views, config, bellSyncDiffMillis) appWidgetManager.updateAppWidget(appWidgetId, views) @@ -402,7 +395,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { } } headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE) - val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentFlag()) + val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent) timetables!!.put(appWidgetId, models) @@ -416,7 +409,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { // create an intent used to display the lesson details dialog val itemIntent = Intent(app, LessonDialogActivity::class.java) itemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK/* or Intent.FLAG_ACTIVITY_CLEAR_TASK*/) - val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, PendingIntent.FLAG_MUTABLE) + val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, pendingIntentMutable()) views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent) if (!unified) From 07ab1b984f9c2e38e5aa16082a14d85c72636ae8 Mon Sep 17 00:00:00 2001 From: "B.O.S.S" Date: Fri, 24 Mar 2023 22:09:03 +0100 Subject: [PATCH 05/10] [API/Librus] Fix login. (#176) --- .../edziennik/data/api/Constants.kt | 8 +- .../edziennik/data/api/Regexes.kt | 19 +++ .../librus/login/LibrusLoginPortal.kt | 159 ++++++++++++------ 3 files changed, 126 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index c29b8698..31fabaea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -26,9 +26,10 @@ val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp" const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0" const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP" const val LIBRUS_REDIRECT_URL = "app://librus" -const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code" -const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action" +const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru" +const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action" const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token" +const val LIBRUS_HEADER = "pl.librus.synergiaDru2" const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts" @@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action=" const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile" const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik" -const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR" -const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login" - val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index f3d5a297..70388abf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -24,6 +24,25 @@ object Regexes { """^\[META:([A-z0-9-&=]+)]""".toRegex() } + val HTML_INPUT_HIDDEN by lazy { + """""".toRegex() + } + val HTML_INPUT_NAME by lazy { + """name="(.+?)"""".toRegex() + } + val HTML_INPUT_VALUE by lazy { + """value="(.+?)"""".toRegex() + } + val HTML_CSRF_TOKEN by lazy { + """name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex() + } + val HTML_FORM_ACTION by lazy { + """
Unit) { private const val TAG = "LoginLibrusPortal" } + // loop failsafe + private var loginPerformed = false + init { run { if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) { data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) @@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) return@run } + loginPerformed = false // succeed having a non-expired access token and a refresh token if (data.isPortalLoginValid()) { @@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { } }} - private fun authorize(url: String?) { + private fun authorize(url: String, referer: String? = null) { d(TAG, "Request: Librus/Login/Portal - $url") Request.builder() .url(url) .userAgent(LIBRUS_USER_AGENT) + .also { + if (referer != null) + it.addHeader("Referer", referer) + } + .addHeader("X-Requested-With", LIBRUS_HEADER) .withClient(data.app.httpLazy) .callback(object : TextCallbackHandler() { override fun onSuccess(text: String, response: Response) { val location = response.headers().get("Location") if (location != null) { - val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) + val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) when { authMatcher.find() -> { accessToken(authMatcher.group(1), null) @@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { authorize(location) } } - } else { - val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text) - if (csrfMatcher.find()) { - login(csrfMatcher.group(1) ?: "") - } else { - data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING) - .withResponse(response) - .withApiResponse(text)) + return + } + + if (checkError(text, response)) + return + + var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL + val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: "" + + for (match in Regexes.HTML_FORM_ACTION.findAll(text)) { + val form = match.value.lowercase() + if ("login" in form && "post" in form) { + loginUrl = match[1] } } + + val params = mutableMapOf() + for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) { + val input = match.value + val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue + val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue + params[name] = value + } + + login(url = loginUrl, referer = url, csrfToken, params) } override fun onFailure(response: Response, throwable: Throwable) { @@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { .enqueue() } - private fun login(csrfToken: String) { - d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}") + private fun checkError(text: String, response: Response): Boolean { + when { + text.contains("librus_account_settings_main") -> return false + text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED + text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN + text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN + else -> null // no error for now + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return true + } + + if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) { + val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1) + if (siteKey == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR) + .withApiResponse(text) + .withResponse(response)) + return true + } + data.requireUserAction( + type = UserActionRequiredEvent.Type.RECAPTCHA, + params = Bundle( + "siteKey" to siteKey, + "referer" to response.request().url().toString(), + "userAgent" to LIBRUS_USER_AGENT, + ), + errorText = R.string.notification_user_action_required_captcha_librus, + ) + return true + } + return false + } + + private fun login( + url: String, + referer: String, + csrfToken: String?, + params: Map, + ) { + if (loginPerformed) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)) + return + } + + d(TAG, "Request: Librus/Login/Portal - $url") val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null) val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L) @@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { Request.builder() .url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL) .userAgent(LIBRUS_USER_AGENT) + .addHeader("X-Requested-With", LIBRUS_HEADER) + .addHeader("Referer", referer) + .withClient(data.app.httpLazy) .addParameter("email", data.portalEmail) .addParameter("password", data.portalPassword) .also { if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */) it.addParameter("g-recaptcha-response", recaptchaCode) + if (csrfToken != null) + it.addHeader("X-CSRF-TOKEN", csrfToken) + for ((key, value) in params) { + it.addParameter(key, value) + } } - .addHeader("X-CSRF-TOKEN", csrfToken) - .allowErrorCode(HTTP_BAD_REQUEST) - .allowErrorCode(HTTP_FORBIDDEN) - .contentType(MediaTypeUtils.APPLICATION_JSON) + .contentType(MediaTypeUtils.APPLICATION_FORM) .post() - .callback(object : JsonCallbackHandler() { - override fun onSuccess(json: JsonObject?, response: Response) { + .callback(object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response) { + loginPerformed = true val location = response.headers()?.get("Location") if (location == "$LIBRUS_REDIRECT_URL?command=close") { data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE) - .withApiResponse(json) + .withApiResponse(text) .withResponse(response)) return } - - if (json == null) { - if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) { - data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED) - .withResponse(response)) - return - } + if (text == null) { data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) .withResponse(response)) return } - val error = if (response.code() == 200) null else - json.getJsonArray("errors")?.getString(0) - ?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString - if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) { - data.requireUserAction( - type = UserActionRequiredEvent.Type.RECAPTCHA, - params = Bundle( - "siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY, - "referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER, - ), - errorText = R.string.notification_user_action_required_captcha_librus, - ) - return - } - - error?.let { code -> - when { - code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED - code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN - code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN - else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR - }.let { errorCode -> - data.error(ApiError(TAG, errorCode) - .withApiResponse(json) - .withResponse(response)) - return - } - } - authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL)) + authorize( + url = location + ?: if (data.fakeLogin) + FAKE_LIBRUS_AUTHORIZE + else + LIBRUS_AUTHORIZE_URL, + referer = referer, + ) } override fun onFailure(response: Response, throwable: Throwable) { From db00566ebf58482352bb8f873aecec5210d029c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 24 Mar 2023 21:25:54 +0100 Subject: [PATCH 06/10] [UI/Login] Fallback reCAPTCHA to WebView activity. --- app/src/main/AndroidManifest.xml | 6 +- .../edziennik/ui/captcha/RecaptchaDialog.kt | 7 +- .../ui/captcha/RecaptchaPromptDialog.kt | 4 +- .../ui/login/recaptcha/RecaptchaActivity.kt | 132 ++++++++++++++++++ .../ui/login/recaptcha/RecaptchaResult.kt | 10 ++ .../utils/managers/UserActionManager.kt | 37 +++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c24b426..da531480 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -160,7 +160,11 @@ + android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" /> + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt index cfecde91..56d3976a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt @@ -25,6 +25,7 @@ class RecaptchaDialog( private val autoRetry: Boolean = true, private val onSuccess: (recaptchaCode: String) -> Unit, private val onFailure: (() -> Unit)? = null, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -44,7 +45,11 @@ class RecaptchaDialog( override suspend fun onBeforeShow(): Boolean { val (title, text, bitmap) = withContext(Dispatchers.Default) { - val html = loadCaptchaHtml() ?: return@withContext null + val html = loadCaptchaHtml() + if (html == null) { + onServerError?.invoke() + return@withContext null + } return@withContext loadCaptchaData(html) } ?: run { onFailure?.invoke() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt index a927347d..ca876ddb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt @@ -19,6 +19,7 @@ class RecaptchaPromptDialog( private val referer: String, private val onSuccess: (recaptchaCode: String) -> Unit, private val onCancel: (() -> Unit)?, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -62,7 +63,8 @@ class RecaptchaPromptDialog( b.checkbox.background = checkboxBackground b.checkbox.foreground = checkboxForeground b.progress.visibility = View.GONE - } + }, + onServerError = onServerError, ).show() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt new file mode 100644 index 00000000..2e58da07 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +import android.annotation.SuppressLint +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.Base64 +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT +import pl.szczodrzynski.edziennik.utils.Themes +import java.nio.charset.Charset + +class RecaptchaActivity : AppCompatActivity() { + companion object { + private const val TAG = "RecaptchaActivity" + + private const val CODE = """ + PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds + ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj + cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxkaXYgaWQ9ImdyIiBzdHlsZT0icG9zaXRpb246YWJzb2x1 + dGU7dG9wOjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKDAsLTUwJSk7Ij48L2Rpdj48YnI+PHNjcmlw + dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCJnciIse3NpdGVrZXk6IlNJVEVL + RVkiLHRoZW1lOiJUSEVNRSIsY2FsbGJhY2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNr + KGUpO30sImV4cGlyZWQtY2FsbGJhY2siOndpbmRvdy5pZi5leHBpcmVkQ2FsbGJhY2ssImVycm9y + LWNhbGxiYWNrIjp3aW5kb3cuaWYuZXJyb3JDYWxsYmFja30pO308L3NjcmlwdD48L2JvZHk+PC9o + dG1sPg== + """ + } + + private var isSuccessful = false + private lateinit var jsInterface: CaptchaCallbackInterface + + interface CaptchaCallbackInterface { + @JavascriptInterface + fun callback(recaptchaResponse: String) + + @JavascriptInterface + fun expiredCallback() + + @JavascriptInterface + fun errorCallback() + } + + @SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.recaptcha_dialog_title) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true) + } + + val siteKey = intent.getStringExtra("siteKey") ?: return + val referer = intent.getStringExtra("referer") ?: return + val userAgent = intent.getStringExtra("userAgent") ?: SYSTEM_USER_AGENT + + val htmlContent = Base64.decode(CODE, Base64.DEFAULT) + .toString(Charset.defaultCharset()) + .replace("THEME", if (Themes.isDark) "dark" else "light") + .replace("SITEKEY", siteKey) + + jsInterface = object : CaptchaCallbackInterface { + @JavascriptInterface + override fun callback(recaptchaResponse: String) { + isSuccessful = true + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = recaptchaResponse, + ) + ) + finish() + } + + @JavascriptInterface + override fun expiredCallback() { + isSuccessful = false + } + + @JavascriptInterface + override fun errorCallback() { + isSuccessful = false + EventBus.getDefault().post( + RecaptchaResult( + isError = true, + code = null, + ) + ) + finish() + } + } + + val webView = WebView(this).apply { + setBackgroundColor(Color.TRANSPARENT) + settings.javaScriptEnabled = true + settings.userAgentString = userAgent + addJavascriptInterface(jsInterface, "if") + loadDataWithBaseURL( + referer, + htmlContent, + "text/html", + "UTF-8", + null, + ) + // setLayerType(WebView.LAYER_TYPE_SOFTWARE, null) + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + setContentView(webView) + } + + override fun onDestroy() { + super.onDestroy() + if (!isSuccessful) + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = null, + ) + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt new file mode 100644 index 00000000..ae5fae19 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +data class RecaptchaResult( + val isError: Boolean, + val code: String?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 3e8761c8..9ca4c233 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -22,6 +22,8 @@ import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaActivity +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaResult import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -107,10 +109,45 @@ class UserActionManager(val app: App) { )) }, onCancel = callback.onCancel, + onServerError = { + executeRecaptchaActivity(activity, event, callback) + }, ).show() return true } + private fun executeRecaptchaActivity( + activity: AppCompatActivity, + event: UserActionRequiredEvent, + callback: UserActionCallback, + ): Boolean { + event.params.getString("siteKey") ?: return false + event.params.getString("referer") ?: return false + + var listener: Any? = null + listener = object { + @Subscribe(threadMode = ThreadMode.MAIN) + fun onRecaptchaResult(result: RecaptchaResult) { + EventBus.getDefault().unregister(listener) + when { + result.isError -> callback.onFailure?.invoke() + result.code != null -> { + finishAction(activity, event, callback, Bundle( + "recaptchaCode" to result.code, + "recaptchaTime" to System.currentTimeMillis(), + )) + } + else -> callback.onCancel?.invoke() + } + } + } + EventBus.getDefault().register(listener) + + val intent = Intent(activity, RecaptchaActivity::class.java).putExtras(event.params) + activity.startActivity(intent) + return true + } + private fun executeOauth( activity: AppCompatActivity, event: UserActionRequiredEvent, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e8e44d0..4620f9b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1543,6 +1543,7 @@ TODO USOS - wymagane logowanie z użyciem przeglądarki Zaloguj się + reCAPTCHA Nie można załadować danych aplikacji {cmd-share-variant} udostępnione w klasie {cmd-share-variant} udostępnione przez Ciebie From 21c00bbe539b0b988cc5e479e3d71efa714bce19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 24 Mar 2023 21:26:46 +0100 Subject: [PATCH 07/10] [API] Fix detecting session cookies. Remove expired cookies. --- .../edziennik/network/cookie/DumbCookie.kt | 11 ++++---- .../edziennik/network/cookie/DumbCookieJar.kt | 28 +++++++++++++++++-- .../edziennik/ui/debug/LabPageFragment.kt | 4 +++ app/src/main/res/layout/lab_fragment.xml | 8 ++++++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt index 53665332..ecd513b0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt @@ -33,7 +33,10 @@ class DumbCookie(var cookie: Cookie) { cookie = Cookie.Builder() .name(cookie.name()) .value(cookie.value()) - .expiresAt(cookie.expiresAt()) + .also { + if (cookie.persistent()) + it.expiresAt(cookie.expiresAt()) + } .domain(cookie.domain()) .build() } @@ -58,8 +61,6 @@ class DumbCookie(var cookie: Cookie) { return hash } - fun serialize(): Pair { - val key = cookie.domain() + "|" + cookie.name() - return key to cookie.toString() - } + fun serializeKey() = cookie.domain() + "|" + cookie.name() + fun serialize() = serializeKey() to cookie.toString() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt index 3bb9e0e5..945e16ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt @@ -30,10 +30,20 @@ class DumbCookieJar( private val sessionCookies = mutableSetOf() init { + val toRemove = mutableListOf() prefs.all.forEach { (key, value) -> if (value !is String) return@forEach - sessionCookies.add(DumbCookie.deserialize(key, value) ?: return@forEach) + val dc = DumbCookie.deserialize(key, value) ?: return@forEach + if (dc.cookie.expiresAt() > System.currentTimeMillis()) + sessionCookies.add(dc) + else + toRemove.add(key) + } + prefs.edit { + for (key in toRemove) { + remove(key) + } } } @@ -48,7 +58,14 @@ class DumbCookieJar( } } private fun delete(vararg toRemove: DumbCookie) { - sessionCookies.removeAll(toRemove) + sessionCookies.removeAll(toRemove.toSet()) + prefs.edit { + for (dc in toRemove) { + val key = dc.serializeKey() + if (prefs.contains(key)) + remove(key) + } + } } override fun saveFromResponse(url: HttpUrl, cookies: MutableList) { @@ -114,4 +131,11 @@ class DumbCookieJar( } delete(*toRemove.toTypedArray()) } + + fun clearAllDomains() { + sessionCookies.clear() + prefs.edit { + clear() + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt index 209d8e82..2e40eefc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt @@ -179,6 +179,10 @@ class LabPageFragment : LazyFragment(), CoroutineScope { return@setOnChangeListener true } + b.clearCookies.onClick { + app.cookieJar.clearAllDomains() + } + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) startCoroutineTimer(500L, 300L) { val text = app.cookieJar.getAllDomains() diff --git a/app/src/main/res/layout/lab_fragment.xml b/app/src/main/res/layout/lab_fragment.xml index e41a66f2..af55e6f0 100644 --- a/app/src/main/res/layout/lab_fragment.xml +++ b/app/src/main/res/layout/lab_fragment.xml @@ -107,6 +107,14 @@ android:text="Reset event types" android:textAllCaps="false" /> +