Validate UUID preferences and fix images loading on MangaDex (#13597)

* Validate UUIDs on preferences.

* Make the MD@H report call asynchronous.
This commit is contained in:
Alessandro Jean 2022-09-26 22:15:52 -03:00 committed by GitHub
parent 8fc2741090
commit c5bd48adc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 47 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'MangaDex' extName = 'MangaDex'
pkgNameSuffix = 'all.mangadex' pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory' extClass = '.MangaDexFactory'
extVersionCode = 167 extVersionCode = 168
isNsfw = true isNsfw = true
} }

View File

@ -6,8 +6,10 @@ import java.util.TimeZone
object MDConstants { object MDConstants {
val uuidRegex = const val UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
val uuidRegex = UUID_REGEX.toRegex()
val onlyUuidRegex = "^$UUID_REGEX$".toRegex()
const val mangaLimit = 20 const val mangaLimit = 20
const val latestChapterLimit = 100 const val latestChapterLimit = 100
@ -103,6 +105,11 @@ object MDConstants {
return "${blockedUploaderPref}_$dexLang" return "${blockedUploaderPref}_$dexLang"
} }
private const val hasSanitizedUuidsPref = "hasSanitizedUuids"
fun getHasSanitizedUuidsPrefKey(dexLang: String): String {
return "${hasSanitizedUuidsPref}_$dexLang"
}
const val tagGroupContent = "content" const val tagGroupContent = "content"
const val tagGroupFormat = "format" const val tagGroupFormat = "format"
const val tagGroupGenre = "genre" const val tagGroupGenre = "genre"

View File

@ -64,6 +64,10 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.addInterceptor(MdAtHomeReportInterceptor(network.client, headers)) .addInterceptor(MdAtHomeReportInterceptor(network.client, headers))
.build() .build()
init {
preferences.sanitizeExistingUuidPrefs()
}
// POPULAR Manga Section // POPULAR Manga Section
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
@ -745,15 +749,11 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
title = helper.intl.blockGroupByUuid title = helper.intl.blockGroupByUuid
summary = helper.intl.blockGroupByUuidSummary summary = helper.intl.blockGroupByUuidSummary
setOnPreferenceChangeListener { _, newValue -> setOnBindEditTextListener(helper::setupEditTextUuidValidator)
val groupsBlocked = newValue.toString()
.split(",")
.map { it.trim() }
.filter { helper.containsUuid(it) }
.joinToString(", ")
setOnPreferenceChangeListener { _, newValue ->
preferences.edit() preferences.edit()
.putString(MDConstants.getBlockedGroupsPrefKey(dexLang), groupsBlocked) .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), newValue.toString())
.commit() .commit()
} }
} }
@ -763,15 +763,11 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
title = helper.intl.blockUploaderByUuid title = helper.intl.blockUploaderByUuid
summary = helper.intl.blockUploaderByUuidSummary summary = helper.intl.blockUploaderByUuidSummary
setOnPreferenceChangeListener { _, newValue -> setOnBindEditTextListener(helper::setupEditTextUuidValidator)
val uploaderBlocked = newValue.toString()
.split(",")
.map { it.trim() }
.filter { helper.containsUuid(it) }
.joinToString(", ")
setOnPreferenceChangeListener { _, newValue ->
preferences.edit() preferences.edit()
.putString(MDConstants.getBlockedUploaderPrefKey(dexLang), uploaderBlocked) .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), newValue.toString())
.commit() .commit()
} }
} }
@ -795,4 +791,28 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
private inline fun <reified T> Response.parseAs(): T = use { private inline fun <reified T> Response.parseAs(): T = use {
helper.json.decodeFromString(body?.string().orEmpty()) helper.json.decodeFromString(body?.string().orEmpty())
} }
private fun SharedPreferences.sanitizeExistingUuidPrefs() {
if (getBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), false)) {
return
}
val blockedGroups = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")!!
.split(",")
.map(String::trim)
.filter(helper::isUuid)
.joinToString(", ")
val blockedUploaders = getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")!!
.split(",")
.map(String::trim)
.filter(helper::isUuid)
.joinToString(", ")
edit()
.putString(MDConstants.getBlockedGroupsPrefKey(dexLang), blockedGroups)
.putString(MDConstants.getBlockedUploaderPrefKey(dexLang), blockedUploaders)
.putBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), true)
.apply()
}
} }

View File

@ -1,6 +1,10 @@
package eu.kanade.tachiyomi.extension.all.mangadex package eu.kanade.tachiyomi.extension.all.mangadex
import android.text.Editable
import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.widget.Button
import android.widget.EditText
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto
@ -19,7 +23,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -62,6 +65,11 @@ class MangaDexHelper(private val lang: String) {
*/ */
fun containsUuid(url: String) = url.contains(MDConstants.uuidRegex) fun containsUuid(url: String) = url.contains(MDConstants.uuidRegex)
/**
* Check if the string is a valid uuid
*/
fun isUuid(text: String) = text.matches(MDConstants.onlyUuidRegex)
/** /**
* Get the manga offset pages are 1 based, so subtract 1 * Get the manga offset pages are 1 based, so subtract 1
*/ */
@ -394,4 +402,33 @@ class MangaDexHelper(private val lang: String) {
currentSlug currentSlug
} }
} }
/**
* Adds a custom [TextWatcher] to the preference's [EditText] that show an
* error if the input value contains invalid UUIDs. If the validation fails,
* the Ok button is disabled to prevent the user from saving the value.
*/
fun setupEditTextUuidValidator(editText: EditText) {
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(editable: Editable?) {
requireNotNull(editable)
val text = editable.toString()
val isValid = text.isBlank() || text
.split(",")
.map(String::trim)
.all(::isUuid)
editText.error = if (!isValid) intl.invalidUuids else null
editText.rootView.findViewById<Button>(android.R.id.button1)
?.isEnabled = editText.error == null
}
})
}
} }

View File

@ -3,14 +3,19 @@ package eu.kanade.tachiyomi.extension.all.mangadex
import java.text.Collator import java.text.Collator
import java.util.Locale import java.util.Locale
class MangaDexIntl(val lang: String) { class MangaDexIntl(lang: String) {
val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH
val locale: Locale = Locale.forLanguageTag(availableLang) private val locale: Locale = Locale.forLanguageTag(availableLang)
val collator: Collator = Collator.getInstance(locale) val collator: Collator = Collator.getInstance(locale)
val invalidUuids: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "O texto contém UUIDs inválidos"
else -> "The text contains invalid UUIDs"
}
val invalidGroupId: String = when (availableLang) { val invalidGroupId: String = when (availableLang) {
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do grupo inválido" BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do grupo inválido"
else -> "Not a valid group ID" else -> "Not a valid group ID"

View File

@ -5,6 +5,8 @@ import eu.kanade.tachiyomi.extension.all.mangadex.dto.ImageReportDto
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -12,6 +14,7 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException
/** /**
* Interceptor to post to md@home for MangaDex Stats * Interceptor to post to md@home for MangaDex Stats
@ -28,37 +31,46 @@ class MdAtHomeReportInterceptor(
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val response = chain.proceed(chain.request())
return chain.proceed(chain.request()).let { response ->
val url = originalRequest.url.toString() val url = originalRequest.url.toString()
if (url.contains(mdAtHomeUrlRegex)) {
val byteSize = response.peekBody(Long.MAX_VALUE).bytes().size if (!url.contains(mdAtHomeUrlRegex)) {
val duration = response.receivedResponseAtMillis - response.sentRequestAtMillis return response
val cache = response.header("X-Cache", "") == "HIT" }
val result = ImageReportDto( val result = ImageReportDto(
url, url,
response.isSuccessful, success = response.isSuccessful,
byteSize, bytes = response.peekBody(Long.MAX_VALUE).bytes().size,
cache, cached = response.header("X-Cache", "") == "HIT",
duration duration = response.receivedResponseAtMillis - response.sentRequestAtMillis
) )
val jsonString = json.encodeToString(result) val payload = json.encodeToString(result)
try { val reportRequest = POST(
client.newCall( url = MDConstants.atHomePostUrl,
POST( headers = headers,
MDConstants.atHomePostUrl, body = payload.toRequestBody(JSON_MEDIA_TYPE)
headers,
jsonString.toRequestBody("application/json".toMediaType())
) )
).execute().close()
} catch (e: Exception) { // Execute the report endpoint network call asynchronously to avoid blocking
// the reader from showing the image once it's fully loaded if the report call
// gets stuck, as it tend to happens sometimes.
client.newCall(reportRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}") Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}")
} }
override fun onResponse(call: Call, response: Response) {
response.close()
}
})
return response
} }
response companion object {
} private val JSON_MEDIA_TYPE = "application/json".toMediaType()
} }
} }