239 lines
9.0 KiB
Kotlin
239 lines
9.0 KiB
Kotlin
/**
|
|
* ProjectLaogai
|
|
*
|
|
* Copyright 2019-2020 <seil0@mosad.xyz>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
* MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
package org.mosad.seil0.projectlaogai.controller
|
|
|
|
import android.content.Context
|
|
import android.util.Log
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.coroutines.withContext
|
|
import org.jsoup.HttpStatusException
|
|
import org.jsoup.Jsoup
|
|
import org.jsoup.nodes.Element
|
|
import org.mosad.seil0.projectlaogai.R
|
|
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
|
|
import org.mosad.seil0.projectlaogai.util.GradeSubject
|
|
import java.security.KeyStore
|
|
import java.security.cert.CertificateFactory
|
|
import java.security.cert.X509Certificate
|
|
import java.util.*
|
|
import javax.net.ssl.SSLContext
|
|
import javax.net.ssl.SSLSocketFactory
|
|
import javax.net.ssl.TrustManagerFactory
|
|
import kotlin.collections.ArrayList
|
|
import kotlin.collections.HashMap
|
|
|
|
/**
|
|
* Parse the qispos site the get all needed data for the grades fragment
|
|
*/
|
|
class QISPOSParser(val context: Context) {
|
|
|
|
private val className = this.javaClass.name
|
|
private val baseURL = "https://notenverwaltung.hs-offenburg.de"
|
|
private val loginPath = "/qispos/rds?state=user&type=1&category=auth.login&startpage=portal.vm&breadCrumbSource=portal"
|
|
|
|
/**
|
|
* check if qispos is available
|
|
* @return a http status code, 999 if no HttpStatusException supplied
|
|
*/
|
|
fun checkQISPOSStatus(): Int {
|
|
return runBlocking {
|
|
withContext(Dispatchers.IO) {
|
|
val socketFactory = createSSLSocketFactory()
|
|
|
|
try {
|
|
val res = Jsoup.connect(baseURL + loginPath)
|
|
.sslSocketFactory(socketFactory)
|
|
.followRedirects(true)
|
|
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
|
|
.execute()
|
|
|
|
Log.i(className, "Qispos status is: ${res.statusCode()}")
|
|
res.statusCode()
|
|
} catch (exHttp: HttpStatusException) {
|
|
Log.w(className, "Qispos status is: ${exHttp.statusCode}")
|
|
exHttp.statusCode
|
|
} catch (ex: Exception) {
|
|
Log.e(className, "Error while checking status", ex)
|
|
999
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* parse the html from readGrades()
|
|
* @return a SortedMap, each entry is a semester, each semester has a ArrayList with subjects
|
|
*/
|
|
fun parseGrades(): SortedMap<String, ArrayList<GradeSubject>> {
|
|
val gradesMap = HashMap<String, ArrayList<GradeSubject>>()
|
|
val gradesListHtml = readGrades()
|
|
|
|
gradesListHtml?.select("table > tbody > tr")?.forEach {
|
|
val row = it.select("td.tabelle1_alignleft,td.tabelle1_aligncenter,td.tabelle1_alignright")
|
|
|
|
// only real subjects will be selected
|
|
if(row.size >= 6 && row[0].text().length >=7) {
|
|
val subject = GradeSubject(
|
|
id = row[0].text(),
|
|
name = row[1].text(),
|
|
semester = row[2].text(),
|
|
grade = if (row[3].text().isNotEmpty()) row[3].text() else row[4].text(),
|
|
credits = row[5].text()
|
|
)
|
|
|
|
if (gradesMap.containsKey(subject.semester)) {
|
|
gradesMap[subject.semester]!!.add(subject)
|
|
} else {
|
|
gradesMap[subject.semester] = arrayListOf(subject)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return gradesMap.toSortedMap(compareBy<String>{
|
|
val oText = it.substringAfter(" ")
|
|
|
|
// if WS, substring before / and add 0.5 to compareBy
|
|
if (oText.contains("/")) {
|
|
oText.substringBefore("/").toInt() + 0.5
|
|
} else {
|
|
oText.toDouble()
|
|
}
|
|
}.thenBy { it })
|
|
}
|
|
|
|
/**
|
|
* read the grades html from qispos
|
|
* @return the grades list as html element or null
|
|
*/
|
|
private fun readGrades(): Element?{
|
|
|
|
val credentials = EncryptedPreferences.readCredentials(context)
|
|
val username = credentials.first.substringBefore("@")
|
|
val password = credentials.second
|
|
|
|
return runBlocking {
|
|
withContext(Dispatchers.IO) {
|
|
try {
|
|
val socketFactory = createSSLSocketFactory()
|
|
|
|
// login, asdf = username, fdsa = password, wtf
|
|
val list = mapOf(
|
|
Pair("asdf", username),
|
|
Pair("fdsa", password),
|
|
Pair("submit", "Anmelden")
|
|
)
|
|
|
|
// login and get session cookies
|
|
val res = Jsoup.connect(baseURL + loginPath)
|
|
.sslSocketFactory(socketFactory)
|
|
.followRedirects(true)
|
|
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
|
|
.data(list)
|
|
.postDataCharset("UTF-8")
|
|
.execute()
|
|
Log.i(className, "Login status is: ${res.statusCode()} (${res.statusMessage()})")
|
|
|
|
val loginCookies = res.cookies()
|
|
|
|
// grades root document and url
|
|
val rootHtml =Jsoup.parse(res.body())
|
|
val gradesRootLink = rootHtml.select("li.menueListStyle > a.auflistung").last().attr("abs:href")
|
|
|
|
// parse grades url
|
|
val gradesHtml = Jsoup.connect(gradesRootLink)
|
|
.sslSocketFactory(socketFactory)
|
|
.followRedirects(true)
|
|
.cookies(loginCookies)
|
|
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
|
|
.get()
|
|
|
|
val gradesNextLink = gradesHtml.select("li.treelist > a.regular").attr("abs:href")
|
|
|
|
val gradesNextHtml = Jsoup.connect(gradesNextLink)
|
|
.sslSocketFactory(socketFactory)
|
|
.followRedirects(true)
|
|
.cookies(loginCookies)
|
|
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
|
|
.get()
|
|
|
|
val gradesListLink = gradesNextHtml.selectFirst("li.treelist > ul > li").selectFirst("a").attr("abs:href")
|
|
|
|
// get the grades list
|
|
val gradesListHtml = Jsoup.connect(gradesListLink)
|
|
.sslSocketFactory(socketFactory)
|
|
.followRedirects(true)
|
|
.cookies(loginCookies)
|
|
.referrer("https://notenverwaltung.hs-offenburg.de/qispos/rds?state=user&type=0")
|
|
.get()
|
|
|
|
Log.i(className, "Read html length: ${gradesListHtml.body().html().length}")
|
|
gradesListHtml.body()
|
|
} catch (ex: Exception) {
|
|
Log.e(className, "Error while loading Qispos", ex)
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* since the HS has a fucked up tls setup we need to work around that
|
|
*/
|
|
private fun createSSLSocketFactory(): SSLSocketFactory {
|
|
// Load CAs from an InputStream
|
|
// (could be from a resource or ByteArrayInputStream or ...)
|
|
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
|
|
val caInput = context.resources.openRawResource(R.raw.notenverwaltung_hs_offenburg_de)
|
|
val ca = caInput.use {
|
|
cf.generateCertificate(it) as X509Certificate
|
|
}
|
|
|
|
// Create a KeyStore containing our trusted CAs
|
|
val keyStoreType = KeyStore.getDefaultType()
|
|
val keyStore = KeyStore.getInstance(keyStoreType).apply {
|
|
load(null, null)
|
|
setCertificateEntry("ca", ca)
|
|
}
|
|
|
|
// Create a TrustManager that trusts the CAs inputStream our KeyStore
|
|
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
|
|
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
|
|
init(keyStore)
|
|
}
|
|
|
|
// Create an SSLContext that uses our TrustManager
|
|
val sslContext = SSLContext.getInstance("TLS").apply {
|
|
init(null, tmf.trustManagers, null)
|
|
}
|
|
|
|
return sslContext.socketFactory
|
|
}
|
|
|
|
|
|
|
|
|
|
} |