Compare commits

...

12 Commits

28 changed files with 245 additions and 149 deletions

View File

@ -211,7 +211,7 @@ dependencies {
implementation 'com.qifan.powerpermission:powerpermission:1.3.0'
implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.3.0'
implementation 'com.github.kuba2k2.FSLogin:lib:master-SNAPSHOT'
implementation 'com.github.kuba2k2.FSLogin:lib:2.0.0'
implementation 'pl.droidsonroids:jspoon:1.3.2'
implementation "com.squareup.retrofit2:converter-scalars:2.8.1"
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"

View File

@ -67,3 +67,6 @@
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo$Platform { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData$Type { *; }

View File

@ -1,8 +1,8 @@
<h3>Wersja 4.5.1-beta.1, 2021-02-22</h3>
<h3>Wersja 4.6-beta.1, 2021-02-25</h3>
<ul>
<li>Vulcan: dodano wyświetlanie <b>szczęśliwego numerka.</b></li>
<li>Vulcan: przywrócono uwagi oraz wysyłanie wiadomości.</li>
<li>Vulcan: usunięto wyświetlanie zadań domowych i sprawdzianów z poprzednich lat.</li>
<li>Vulcan: dodano możliwość logowania adresem e-mail lub nazwą użytkownika.</li>
<li>Vulcan: dodano obsługę załączników w wiadomościach i zadaniach domowych.</li>
<li>Zalecane jest zalogowanie się ponownie.</li>
</ul>
<br>
<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x42, 0xe5, 0x9a, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0xe5, 0x07, 0x0f, 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -43,10 +43,9 @@ import androidx.viewpager.widget.ViewPager
import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.*
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@ -703,6 +702,21 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
}
}
fun JsonObject.toBundle(): Bundle {
return Bundle().also {
for ((key, value) in this.entrySet()) {
when (value) {
is JsonObject -> it.putBundle(key, value.toBundle())
is JsonPrimitive -> when {
value.isString -> it.putString(key, value.asString)
value.isBoolean -> it.putBoolean(key, value.asBoolean)
value.isNumber -> it.putInt(key, value.asInt)
}
}
}
}
}
fun JsonArray(vararg properties: Any?): JsonArray {
return JsonArray().apply {
for (property in properties) {

View File

@ -129,9 +129,7 @@ val vulcanLoginMethods = listOf(
.withIsPossible { _, loginStore ->
loginStore.mode != LOGIN_MODE_VULCAN_API
}
.withRequiredLoginMethod { _, loginStore ->
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED
}
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
val idziennikLoginMethods = listOf(

View File

@ -156,7 +156,7 @@ object Regexes {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}
val VULCAN_WEB_PERMISSIONS by lazy {
"""permissions: '([A-z0-9/=+\-_]+?)'""".toRegex()
"""permissions: '([A-z0-9/=+\-_|]+?)'""".toRegex()
}
val VULCAN_WEB_SYMBOL_VALIDATE by lazy {
"""[A-z0-9]+""".toRegex(IGNORE_CASE)

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
@ -14,14 +15,13 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.fslogin.realm.RealmData
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isWebMainLoginValid() = webExpiryTime-30 > currentTimeUnix()
&& webAuthCookie.isNotNullNorEmpty()
&& webHost.isNotNullNorEmpty()
&& webType.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
fun isWebMainLoginValid() = symbol.isNotNullNorEmpty()
&& (webExpiryTime[symbol]?.toLongOrNull() ?: 0) - 30 > currentTimeUnix()
&& webAuthCookie[symbol].isNotNullNorEmpty()
&& webRealmData != null
fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix()
&& apiFingerprint[symbol].isNotNullNorEmpty()
&& apiPrivateKey[symbol].isNotNullNorEmpty()
@ -297,49 +297,15 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
\/ \/ \___|_.__/ |_| |_____/ |______\___/ \__, |_|_| |_|
__/ |
|__*/
/**
* Federation Services login type.
* This might be one of: cufs, adfs, adfslight.
*/
var webType: String?
get() { mWebType = mWebType ?: loginStore.getLoginData("webType", null); return mWebType }
set(value) { loginStore.putLoginData("webType", value); mWebType = value }
private var mWebType: String? = null
var webRealmData: RealmData?
get() { mWebRealmData = mWebRealmData ?: loginStore.getLoginData("webRealmData", JsonObject()).let {
app.gson.fromJson(it, RealmData::class.java)
}; return mWebRealmData }
set(value) { loginStore.putLoginData("webRealmData", app.gson.toJsonTree(value) as JsonObject); mWebRealmData = value }
private var mWebRealmData: RealmData? = null
/**
* Web server providing the federation services login.
* If this is present, WEB_MAIN login is considered as available.
*/
var webHost: String?
get() { mWebHost = mWebHost ?: loginStore.getLoginData("webHost", null); return mWebHost }
set(value) { loginStore.putLoginData("webHost", value); mWebHost = value }
private var mWebHost: String? = null
/**
* An ID used in ADFS & ADFSLight login types.
*/
var webAdfsId: String?
get() { mWebAdfsId = mWebAdfsId ?: loginStore.getLoginData("webAdfsId", null); return mWebAdfsId }
set(value) { loginStore.putLoginData("webAdfsId", value); mWebAdfsId = value }
private var mWebAdfsId: String? = null
/**
* A domain override for ADFS Light.
*/
var webAdfsDomain: String?
get() { mWebAdfsDomain = mWebAdfsDomain ?: loginStore.getLoginData("webAdfsDomain", null); return mWebAdfsDomain }
set(value) { loginStore.putLoginData("webAdfsDomain", value); mWebAdfsDomain = value }
private var mWebAdfsDomain: String? = null
var webIsHttpCufs: Boolean
get() { mWebIsHttpCufs = mWebIsHttpCufs ?: loginStore.getLoginData("webIsHttpCufs", false); return mWebIsHttpCufs ?: false }
set(value) { loginStore.putLoginData("webIsHttpCufs", value); mWebIsHttpCufs = value }
private var mWebIsHttpCufs: Boolean? = null
var webIsScopedAdfs: Boolean
get() { mWebIsScopedAdfs = mWebIsScopedAdfs ?: loginStore.getLoginData("webIsScopedAdfs", false); return mWebIsScopedAdfs ?: false }
set(value) { loginStore.putLoginData("webIsScopedAdfs", value); mWebIsScopedAdfs = value }
private var mWebIsScopedAdfs: Boolean? = null
val webHost
get() = webRealmData?.host
var webEmail: String?
get() { mWebEmail = mWebEmail ?: loginStore.getLoginData("webEmail", null); return mWebEmail }
@ -359,24 +325,24 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
* If the time passes, the certificate needs to be POSTed again (if valid)
* or re-generated.
*/
var webExpiryTime: Long
get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L }
set(value) { profile?.putStudentData("webExpiryTime", value); mWebExpiryTime = value }
private var mWebExpiryTime: Long? = null
var webExpiryTime: Map<String, String?> = mapOf()
get() { mWebExpiryTime = mWebExpiryTime ?: loginStore.getLoginData("webExpiryTime", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebExpiryTime ?: mapOf() }
set(value) { loginStore.putLoginData("webExpiryTime", app.gson.toJson(value)); mWebExpiryTime = value }
private var mWebExpiryTime: Map<String, String?>? = null
/**
* EfebSsoAuthCookie retrieved after posting a certificate
*/
var webAuthCookie: String?
get() { mWebAuthCookie = mWebAuthCookie ?: profile?.getStudentData("webAuthCookie", null); return mWebAuthCookie }
set(value) { profile?.putStudentData("webAuthCookie", value); mWebAuthCookie = value }
private var mWebAuthCookie: String? = null
var webAuthCookie: Map<String, String?> = mapOf()
get() { mWebAuthCookie = mWebAuthCookie ?: loginStore.getLoginData("webAuthCookie", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebAuthCookie ?: mapOf() }
set(value) { loginStore.putLoginData("webAuthCookie", app.gson.toJson(value)); mWebAuthCookie = value }
private var mWebAuthCookie: Map<String, String?>? = null
/**
* Permissions needed to get JSONs from home page
*/
var webPermissions: String?
get() { mWebPermissions = mWebPermissions ?: profile?.getStudentData("webPermissions", null); return mWebPermissions }
set(value) { profile?.putStudentData("webPermissions", value); mWebPermissions = value }
private var mWebPermissions: String? = null
var webPermissions: Map<String, String?> = mapOf()
get() { mWebPermissions = mWebPermissions ?: loginStore.getLoginData("webPermissions", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebPermissions ?: mapOf() }
set(value) { loginStore.putLoginData("webPermissions", app.gson.toJson(value)); mWebPermissions = value }
private var mWebPermissions: Map<String, String?>? = null
}

View File

@ -200,6 +200,14 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getEvent(eventFull: EventFull) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
eventFull.homeworkBody = ""
EventBus.getDefault().postSticky(EventGetEvent(eventFull))
completed()
return
}
login(LOGIN_METHOD_VULCAN_API) {
val list = data.app.db.eventDao().getAllNow(data.profileId).filter { !it.addedManually }
VulcanApiAttachments(data, list, eventFull, EventFull::class) { _ ->

View File

@ -22,13 +22,15 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
private var firstSemesterSync = false
private val firstSemesterSyncExclude = listOf(
ENDPOINT_VULCAN_HEBE_MAIN,
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK,
ENDPOINT_VULCAN_HEBE_TIMETABLE,
ENDPOINT_VULCAN_HEBE_EXAMS,
ENDPOINT_VULCAN_HEBE_HOMEWORK,
ENDPOINT_VULCAN_HEBE_NOTICES,
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX,
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT,
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER
)
init { run {

View File

@ -11,12 +11,11 @@ import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.droidsonroids.jspoon.Jspoon
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.isNotNullNorBlank
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
import java.io.File
@ -83,10 +82,12 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
onResult(symbol, STATE_NO_REGISTER)
return
}
if (!validateCallback(text, response, jsonResponse = false)) {
if (!validateCallback(symbol, text, response, jsonResponse = false)) {
return
}
data.webExpiryTime = Date.fromIso(certificate.expiryDate) / 1000L
data.webExpiryTime = data.webExpiryTime.toMutableMap().also { map ->
map[symbol] = (Date.fromIso(certificate.expiryDate) / 1000L).toString()
}
onResult(symbol, STATE_SUCCESS)
}
@ -120,7 +121,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
fun getStartPage(symbol: String = data.symbol ?: "default", postErrors: Boolean = true, onSuccess: (html: String, schoolSymbols: List<String>) -> Unit) {
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (!validateCallback(text, response, jsonResponse = false) || text == null) {
if (!validateCallback(symbol, text, response, jsonResponse = false) || text == null) {
return
}
@ -136,7 +137,30 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
}
}
data.webPermissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] }
data.webPermissions = data.webPermissions.toMutableMap().also { map ->
val permissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] }
if (permissions?.isNotBlank() == true) {
val studentId = permissions.split("|")
.getOrNull(0)
?.base64DecodeToString()
?.toJsonObject()
?.getJsonArray("AuthInfos")
?.asJsonObjectList()
?.flatMap { authInfo ->
authInfo.getJsonArray("UczenIds")
?.map { it.asInt }
?: listOf()
}
?.firstOrNull()
?.toString()
data.app.cookieJar.set(
data.webHost ?: "vulcan.net.pl",
"idBiezacyUczen",
studentId
)
}
map[symbol] = permissions
}
val schoolSymbols = mutableListOf<String>()
val clientUrl = "://uonetplus-uczen.${data.webHost}/$symbol/"
@ -186,7 +210,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
.enqueue()
}
private fun validateCallback(text: String?, response: Response?, jsonResponse: Boolean = true): Boolean {
private fun validateCallback(symbol: String, text: String?, response: Response?, jsonResponse: Boolean = true): Boolean {
if (text == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
@ -207,11 +231,13 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
val cookies = data.app.cookieJar.getAll(data.webHost ?: "vulcan.net.pl")
val authCookie = cookies["EfebSsoAuthCookie"]
if ((authCookie == null || authCookie == "null") && data.webAuthCookie != null) {
data.app.cookieJar.set(data.webHost ?: "vulcan.net.pl", "EfebSsoAuthCookie", data.webAuthCookie)
if ((authCookie == null || authCookie == "null") && data.webAuthCookie[symbol] != null) {
data.app.cookieJar.set(data.webHost ?: "vulcan.net.pl", "EfebSsoAuthCookie", data.webAuthCookie[symbol])
}
else if (authCookie.isNotNullNorBlank() && authCookie != "null" && authCookie != data.webAuthCookie) {
data.webAuthCookie = authCookie
else if (authCookie.isNotNullNorBlank() && authCookie != "null" && authCookie != data.webAuthCookie[symbol]) {
data.webAuthCookie = data.webAuthCookie.toMutableMap().also { map ->
map[symbol] = authCookie
}
}
return true
}
@ -250,7 +276,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (!validateCallback(text, response))
if (!validateCallback(data.symbol ?: "default", text, response))
return
try {

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_HOMEWORK
@ -11,8 +12,10 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanHebeHomework(
override val data: DataVulcan,
@ -56,6 +59,22 @@ class VulcanHebeHomework(
teamId = teamId
)
val attachments = exam.getJsonArray("Attachments")
?.asJsonObjectList()
?: return@forEach
for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue
val attachmentName = "$fileName:$url"
val attachmentId = Utils.crc32(attachmentName.toByteArray())
eventObject.addAttachment(
id = attachmentId,
name = attachmentName
)
}
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.crc16
import kotlin.text.replace
@ -110,6 +111,23 @@ class VulcanHebeMessages(
data.messageRecipientList.add(messageRecipientObject)
}
val attachments = message.getJsonArray("Attachments")
?.asJsonObjectList()
?: return@forEach
for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue
val attachmentName = "$fileName:$url"
val attachmentId = Utils.crc32(attachmentName.toByteArray())
messageObject.addAttachment(
id = attachmentId,
name = attachmentName,
size = -1
)
}
data.messageList.add(messageObject)
data.setSeenMetadataList.add(
Metadata(

View File

@ -13,7 +13,7 @@ import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.fslogin.FSLogin
import pl.szczodrzynski.fslogin.realm.CufsRealm
import pl.szczodrzynski.fslogin.realm.toRealm
class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
companion object {
@ -30,8 +30,7 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
}
else {
if (data.symbol.isNotNullNorEmpty()
&& data.webType.isNotNullNorEmpty()
&& data.webHost.isNotNullNorEmpty()
&& data.webRealmData != null
&& (data.webEmail.isNotNullNorEmpty() || data.webUsername.isNotNullNorEmpty())
&& data.webPassword.isNotNullNorEmpty()) {
try {
@ -56,32 +55,28 @@ class VulcanLoginWebMain(val data: DataVulcan, val onSuccess: () -> Unit) {
data.symbol = getString("symbol")
remove("symbol")
}
// 4.6 - form inputs renamed
if (has("email")) {
data.webEmail = getString("email")
remove("email")
}
if (has("username")) {
data.webUsername = getString("username")
remove("username")
}
if (has("password")) {
data.webPassword = getString("password")
remove("password")
}
}
if (data.symbol == null && data.webRealmData != null) {
data.symbol = data.webRealmData?.symbol
}
}
private fun loginWithCredentials(): Boolean {
val realm = when (data.webType) {
"cufs" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
)
"adfs" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
).toAdfsRealm(id = data.webAdfsId ?: return false)
"adfslight" -> CufsRealm(
host = data.webHost ?: return false,
symbol = data.symbol ?: "default",
httpCufs = data.webIsHttpCufs
).toAdfsLightRealm(
id = data.webAdfsId ?: return false,
domain = data.webAdfsDomain ?: "adfslight",
isScoped = data.webIsScopedAdfs
)
else -> return false
}
val realm = data.webRealmData?.toRealm() ?: return false
val certificate = web.readCertificate()?.let { web.parseCertificate(it) }
if (certificate != null && Date.fromIso(certificate.expiryDate) > System.currentTimeMillis()) {

View File

@ -348,8 +348,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
}
@Throws(Exception::class)
fun getPlatforms(registerName: String): List<LoginInfo.Platform> {
val response = api.appLoginPlatforms(registerName).execute()
fun getRealms(registerName: String): List<LoginInfo.Platform> {
val response = api.fsLoginRealms(registerName).execute()
return parseResponse(response)
}

View File

@ -33,12 +33,12 @@ interface SzkolnyService {
@POST("feedbackMessage")
fun feedbackMessage(@Body request: FeedbackMessageRequest): Call<ApiResponse<FeedbackMessageResponse>>
@GET("appLogin/platforms/{registerName}")
fun appLoginPlatforms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
@GET("firebase/token/{registerName}")
fun firebaseToken(@Path("registerName") registerName: String): Call<ApiResponse<String>>
@GET("registerAvailability")
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
@GET("fsLogin/{registerName}")
fun fsLoginRealms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
}

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MD8+8LPFtX===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MDAwogntL3===.$param2".sha256()
}
}

View File

@ -93,6 +93,25 @@ open class Event(
var attachmentIds: MutableList<Long>? = null
var attachmentNames: MutableList<String>? = null
/**
* Add an attachment
* @param id attachment ID
* @param name file name incl. extension
* @return a Event to which the attachment has been added
*/
fun addAttachment(id: Long, name: String): Event {
if (attachmentIds == null) attachmentIds = mutableListOf()
if (attachmentNames == null) attachmentNames = mutableListOf()
attachmentIds?.add(id)
attachmentNames?.add(name)
return this
}
fun clearAttachments() {
attachmentIds = null
attachmentNames = null
}
@Ignore
var showAsUnseen: Boolean? = null

View File

@ -59,6 +59,7 @@ class LoginStore(
is Long -> putLoginData(key, o)
is Float -> putLoginData(key, o)
is Boolean -> putLoginData(key, o)
is Bundle -> putLoginData(key, o.toJsonObject())
}
}
}

View File

@ -17,7 +17,8 @@ import kotlin.coroutines.CoroutineContext
class ProfileRemoveDialog(
val activity: MainActivity,
val profileId: Int,
val profileName: String
val profileName: String,
val noProfileRemoval: Boolean = false
) : CoroutineScope {
companion object {
private const val TAG = "ProfileRemoveDialog"
@ -52,7 +53,6 @@ class ProfileRemoveDialog(
app.db.attendanceDao().clear(profileId)
app.db.attendanceTypeDao().clear(profileId)
app.db.classroomDao().clear(profileId)
app.db.configDao().clear(profileId)
app.db.endpointTimerDao().clear(profileId)
app.db.eventDao().clear(profileId)
app.db.eventTypeDao().clear(profileId)
@ -65,23 +65,27 @@ class ProfileRemoveDialog(
app.db.messageRecipientDao().clear(profileId)
app.db.noticeDao().clear(profileId)
app.db.noticeTypeDao().clear(profileId)
app.db.noticeTypeDao().clear(profileId)
app.db.notificationDao().clear(profileId)
app.db.subjectDao().clear(profileId)
app.db.teacherAbsenceDao().clear(profileId)
app.db.teacherAbsenceDao().clear(profileId)
app.db.teacherAbsenceTypeDao().clear(profileId)
app.db.teacherDao().clear(profileId)
app.db.teamDao().clear(profileId)
app.db.timetableDao().clear(profileId)
app.db.metadataDao().deleteAll(profileId)
if (noProfileRemoval)
return@async
app.db.configDao().clear(profileId)
val loginStoreId = profileObject.loginStoreId
val profilesUsingLoginStore = app.db.profileDao().getIdsByLoginStoreIdNow(loginStoreId)
if (profilesUsingLoginStore.size == 1) {
app.db.loginStoreDao().remove(loginStoreId)
}
app.db.profileDao().remove(profileId)
app.db.metadataDao().deleteAll(profileId)
if (App.profileId == profileId) {
app.profileLoadLast { }

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.fslogin.decode
@ -60,6 +61,15 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
b.rodo.onClick {
app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}"))
}
b.fullSync.onClick {
app.db.query(SimpleSQLiteQuery("UPDATE profiles SET empty = 1 WHERE profileId = ${App.profileId}"))
app.db.query(SimpleSQLiteQuery("DELETE FROM endpointTimers WHERE profileId = ${App.profileId}"))
}
b.clearProfile.onClick {
ProfileRemoveDialog(activity, App.profileId, "FAKE", noProfileRemoval = true)
}
b.removeHomework.onClick {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")

View File

@ -14,7 +14,6 @@ import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.JsonParser
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
@ -72,7 +71,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
val platformGuideText = arguments?.getString("platformGuideText")
val platformDescription = arguments?.getString("platformDescription")
val platformFormFields = arguments?.getString("platformFormFields")?.split(";")
val platformApiData = arguments?.getString("platformApiData")?.let { JsonParser().parse(it)?.asJsonObject }
val platformRealmData = arguments?.getString("platformRealmData")?.toJsonObject()
b.title.setText(R.string.login_form_title_format, app.getString(register.registerName))
b.subTitle.text = platformName ?: app.getString(mode.name)
@ -159,9 +158,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
payload.putBoolean("fakeLogin", true)
}
platformApiData?.entrySet()?.forEach {
payload.putString(it.key, it.value.asString)
}
payload.putBundle("webRealmData", platformRealmData?.toBundle())
var hasErrors = false
credentials.forEach { (credential, b) ->

View File

@ -6,12 +6,12 @@ package pl.szczodrzynski.edziennik.ui.modules.login
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.google.gson.JsonObject
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel
import pl.szczodrzynski.fslogin.realm.RealmData
object LoginInfo {
@ -191,9 +191,9 @@ object LoginInfo {
isDevOnly = true,
isPlatformSelection = true,
credentials = listOf(
getEmailCredential("webEmail"),
getEmailCredential("email"),
FormField(
keyName = "webUsername",
keyName = "username",
name = R.string.login_hint_username,
icon = CommunityMaterial.Icon.cmd_account_outline,
emptyText = R.string.login_error_no_username,
@ -203,7 +203,7 @@ object LoginInfo {
validationRegex = "[A-Z]{7}[0-9]+",
caseMode = FormField.CaseMode.UPPER_CASE
),
getPasswordCredential("webPassword")
getPasswordCredential("password")
),
errorCodes = mapOf()
)
@ -407,15 +407,12 @@ object LoginInfo {
data class Platform(
val id: Int,
val loginType: Int,
val loginMode: Int,
val name: String,
val description: String?,
val guideText: String?,
val icon: String,
val screenshot: String?,
val formFields: List<String>,
val apiData: JsonObject
val realmData: RealmData
)
open class BaseCredential(

View File

@ -60,12 +60,12 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
adapter = LoginPlatformAdapter(activity) { platform ->
nav.navigate(R.id.loginFormFragment, Bundle(
"loginType" to platform.loginType,
"loginMode" to platform.loginMode,
"loginType" to loginType,
"loginMode" to loginMode,
"platformName" to platform.name,
"platformDescription" to platform.description,
"platformFormFields" to platform.formFields.joinToString(";"),
"platformApiData" to platform.apiData.toString()
"platformRealmData" to app.gson.toJson(platform.realmData)
), activity.navOptions)
}
@ -96,7 +96,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope {
val platforms = LoginInfo.platformList[mode.name]
?: run {
api.runCatching(activity) {
getPlatforms(register.internalName)
getRealms(register.internalName)
} ?: run {
nav.navigateUp()
return@launch

View File

@ -258,7 +258,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.color(IconicsColor.colorInt(iconColor))
)
.setOnClickAction(() -> {
new ProfileRemoveDialog(activity, app.getProfile().getId(), app.getProfile().getName());
new ProfileRemoveDialog(activity, app.getProfile().getId(), app.getProfile().getName(), false);
})
);

View File

@ -121,19 +121,21 @@ class AttachmentsView @JvmOverloads constructor(
// open file by name, or first part before ':' (Vulcan OneDrive)
Utils.openFile(context, File(Utils.getStorageDir(), attachment.name.substringBefore(":")))
return
}
} else if (attachment.name.contains(":")) {
Utils.openUrl(context, attachment.name.substringAfter(":"))
} else {
attachment.isDownloading = true
(adapter as? AttachmentAdapter)?.let {
it.notifyItemChanged(it.items.indexOf(attachment))
}
attachment.isDownloading = true
(adapter as? AttachmentAdapter)?.let {
it.notifyItemChanged(it.items.indexOf(attachment))
}
EdziennikTask.attachmentGet(
EdziennikTask.attachmentGet(
attachment.profileId,
attachment.owner,
attachment.id,
attachment.name
).enqueue(context)
).enqueue(context)
}
}
private val lastUpdate: Long = 0

View File

@ -45,6 +45,22 @@
android:text="Set last 10 as unseen"
android:textAllCaps="false" />
<Button
android:id="@+id/fullSync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Full sync and empty profile"
android:textAllCaps="false" />
<Button
android:id="@+id/clearProfile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="Clear all profile data"
android:textAllCaps="false" />
<Button
android:id="@+id/rodo"
android:layout_width="match_parent"
@ -70,6 +86,7 @@
android:id="@+id/unarchive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Unarchive this profile"
android:textAllCaps="false" />

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.4.30'
release = [
versionName: "4.5.1-beta.1",
versionCode: 4050101
versionName: "4.6-beta.1",
versionCode: 4060001
]
setup = [