Basic certificate URI validation

This commit is contained in:
Aidan Follestad 2019-01-11 19:07:51 -08:00
commit cd1651672f
7 changed files with 84 additions and 31 deletions

View file

@ -28,7 +28,7 @@ import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.ui.DarkModeSwitchActivity import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.ui.viewsite.KEY_SITE import com.afollestad.nocknock.ui.viewsite.KEY_SITE
import com.afollestad.nocknock.utilities.ext.onTextChanged import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.toUri import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.onScroll import com.afollestad.nocknock.viewcomponents.ext.onScroll
@ -141,6 +141,13 @@ class AddSiteActivity : DarkModeSwitchActivity() {
minutesData = viewModel.retryPolicyMinutes minutesData = viewModel.retryPolicyMinutes
) )
// SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
viewModel.onCertificateError()
.toViewError(this, sslCertificateInput)
// Headers // Headers
headersLayout.attach(viewModel.headers) headersLayout.attach(viewModel.headers)
} }
@ -179,9 +186,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
} }
// SSL certificate // SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it.toUri() }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setText(it.toString()) })
sslCertificateBrowse.setOnClickListener { sslCertificateBrowse.setOnClickListener {
val intent = Intent(ACTION_OPEN_DOCUMENT).apply { val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
addCategory(CATEGORY_OPENABLE) addCategory(CATEGORY_OPENABLE)
@ -193,12 +197,11 @@ class AddSiteActivity : DarkModeSwitchActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
appToolbar.elevation = appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
if (scrollView.scrollY > appToolbar.measuredHeight / 2) { appToolbar.dimenFloat(R.dimen.default_elevation)
appToolbar.dimenFloat(R.dimen.default_elevation) } else {
} else { 0f
0f }
}
} }
override fun onActivityResult( override fun onActivityResult(

View file

@ -15,7 +15,6 @@
*/ */
package com.afollestad.nocknock.ui.addsite package com.afollestad.nocknock.ui.addsite
import android.net.Uri
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE import androidx.annotation.VisibleForTesting.PRIVATE
@ -40,6 +39,7 @@ import com.afollestad.nocknock.data.putSite
import com.afollestad.nocknock.engine.validation.ValidationExecutor import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.MINUTE import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.toUri
import com.afollestad.nocknock.utilities.livedata.map import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -69,7 +69,7 @@ class AddSiteViewModel(
val retryPolicyTimes = MutableLiveData<Int>() val retryPolicyTimes = MutableLiveData<Int>()
val retryPolicyMinutes = MutableLiveData<Int>() val retryPolicyMinutes = MutableLiveData<Int>()
val headers = MutableLiveData<List<Header>>() val headers = MutableLiveData<List<Header>>()
val certificateUri = MutableLiveData<Uri>() val certificateUri = MutableLiveData<String>()
@OnLifecycleEvent(ON_START) @OnLifecycleEvent(ON_START)
fun setDefaults() { fun setDefaults() {
@ -91,6 +91,7 @@ class AddSiteViewModel(
private val validationSearchTermError = MutableLiveData<Int?>() private val validationSearchTermError = MutableLiveData<Int?>()
private val validationScriptError = MutableLiveData<Int?>() private val validationScriptError = MutableLiveData<Int?>()
private val checkIntervalValueError = MutableLiveData<Int?>() private val checkIntervalValueError = MutableLiveData<Int?>()
private val certificateError = MutableLiveData<Int?>()
// Expose private properties or calculated properties // Expose private properties or calculated properties
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading @CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
@ -130,6 +131,8 @@ class AddSiteViewModel(
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError @CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
@CheckResult fun onCertificateError(): LiveData<Int?> = certificateError
// Actions // Actions
fun commit(done: () -> Unit) { fun commit(done: () -> Unit) {
scope.launch { scope.launch {
@ -228,6 +231,25 @@ class AddSiteViewModel(
validationScriptError.value = null validationScriptError.value = null
} }
// Validate SSL certificate
val certString = certificateUri.value
if (certString != null) {
val rawCertUri = certString.toUri()
val certUri = if (rawCertUri.scheme == null) {
rawCertUri.buildUpon()
.scheme("file")
.build()
} else {
rawCertUri
}
if (certUri.scheme != "content" && certUri.scheme != "file") {
errorCount++
certificateError.value = R.string.please_enter_validCertUri
} else {
certificateError.value = null
}
}
if (errorCount > 0) { if (errorCount > 0) {
return null return null
} }

View file

@ -28,7 +28,7 @@ import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.ui.DarkModeSwitchActivity import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.utilities.ext.onTextChanged import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.toUri import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
@ -158,6 +158,13 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
minutesData = viewModel.retryPolicyMinutes minutesData = viewModel.retryPolicyMinutes
) )
// SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
viewModel.onCertificateError()
.toViewError(this, sslCertificateInput)
// Headers // Headers
headersLayout.attach(viewModel.headers) headersLayout.attach(viewModel.headers)
@ -214,7 +221,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
.isVisible = it .isVisible = it
}) })
// Done button // Done item text
viewModel.onDoneButtonText() viewModel.onDoneButtonText()
.observe(this, Observer { .observe(this, Observer {
toolbar.menu.findItem(R.id.commit) toolbar.menu.findItem(R.id.commit)
@ -222,9 +229,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
}) })
// SSL certificate // SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it.toUri() }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setText(it.toString()) })
sslCertificateBrowse.setOnClickListener { sslCertificateBrowse.setOnClickListener {
val intent = Intent(ACTION_OPEN_DOCUMENT).apply { val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
addCategory(CATEGORY_OPENABLE) addCategory(CATEGORY_OPENABLE)
@ -236,12 +240,11 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
appToolbar.elevation = appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
if (scrollView.scrollY > appToolbar.measuredHeight / 2) { appToolbar.dimenFloat(R.dimen.default_elevation)
appToolbar.dimenFloat(R.dimen.default_elevation) } else {
} else { 0f
0f }
}
} }
override fun onActivityResult( override fun onActivityResult(

View file

@ -15,7 +15,6 @@
*/ */
package com.afollestad.nocknock.ui.viewsite package com.afollestad.nocknock.ui.viewsite
import android.net.Uri
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE import androidx.annotation.VisibleForTesting.PRIVATE
@ -24,9 +23,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.AppDatabase import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.deleteSite import com.afollestad.nocknock.data.deleteSite
import com.afollestad.nocknock.data.model.Header import com.afollestad.nocknock.data.model.Header
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.Status import com.afollestad.nocknock.data.model.Status
import com.afollestad.nocknock.data.model.Status.WAITING import com.afollestad.nocknock.data.model.Status.WAITING
@ -41,6 +40,7 @@ import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.formatDate import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.ext.toUri
import com.afollestad.nocknock.utilities.livedata.map import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.utilities.livedata.zip import com.afollestad.nocknock.utilities.livedata.zip
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
@ -77,7 +77,7 @@ class ViewSiteViewModel(
val retryPolicyTimes = MutableLiveData<Int>() val retryPolicyTimes = MutableLiveData<Int>()
val retryPolicyMinutes = MutableLiveData<Int>() val retryPolicyMinutes = MutableLiveData<Int>()
val headers = MutableLiveData<List<Header>>() val headers = MutableLiveData<List<Header>>()
val certificateUri = MutableLiveData<Uri>() val certificateUri = MutableLiveData<String>()
internal val disabled = MutableLiveData<Boolean>() internal val disabled = MutableLiveData<Boolean>()
internal val lastResult = MutableLiveData<ValidationResult?>() internal val lastResult = MutableLiveData<ValidationResult?>()
@ -89,6 +89,7 @@ class ViewSiteViewModel(
private val validationSearchTermError = MutableLiveData<Int?>() private val validationSearchTermError = MutableLiveData<Int?>()
private val validationScriptError = MutableLiveData<Int?>() private val validationScriptError = MutableLiveData<Int?>()
private val checkIntervalValueError = MutableLiveData<Int?>() private val checkIntervalValueError = MutableLiveData<Int?>()
private val certificateError = MutableLiveData<Int?>()
// Expose private properties or calculated properties // Expose private properties or calculated properties
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading @CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
@ -131,6 +132,8 @@ class ViewSiteViewModel(
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = @CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> =
disabled.map { !it } disabled.map { !it }
@CheckResult fun onCertificateError(): LiveData<Int?> = certificateError
@CheckResult fun onDoneButtonText(): LiveData<Int> = @CheckResult fun onDoneButtonText(): LiveData<Int> =
disabled.map { disabled.map {
if (it) R.string.renable_and_save_changes if (it) R.string.renable_and_save_changes
@ -307,6 +310,25 @@ class ViewSiteViewModel(
validationScriptError.value = null validationScriptError.value = null
} }
// Validate SSL certificate
val certString = certificateUri.value
if (certString != null) {
val rawCertUri = certString.toUri()
val certUri = if (rawCertUri.scheme == null) {
rawCertUri.buildUpon()
.scheme("file")
.build()
} else {
rawCertUri
}
if (certUri.scheme != "content" && certUri.scheme != "file") {
errorCount++
certificateError.value = R.string.please_enter_validCertUri
} else {
certificateError.value = null
}
}
if (errorCount > 0) { if (errorCount > 0) {
return null return null
} }

View file

@ -24,7 +24,6 @@ import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK import com.afollestad.nocknock.utilities.ext.WEEK
import com.afollestad.nocknock.utilities.ext.toUri
import kotlin.math.ceil import kotlin.math.ceil
fun ViewSiteViewModel.setModel(site: Site) { fun ViewSiteViewModel.setModel(site: Site) {
@ -56,9 +55,7 @@ fun ViewSiteViewModel.setModel(site: Site) {
setCheckInterval(settings.validationIntervalMs) setCheckInterval(settings.validationIntervalMs)
setRetryPolicy(site.retryPolicy) setRetryPolicy(site.retryPolicy)
headers.value = site.headers headers.value = site.headers
if (settings.certificate != null) { certificateUri.value = settings.certificate
certificateUri.value = settings.certificate!!.toUri()
}
this.disabled.value = settings.disabled this.disabled.value = settings.disabled
this.lastResult.value = site.lastResult this.lastResult.value = site.lastResult

View file

@ -33,6 +33,7 @@
<string name="please_enter_search_term">Please input a search term.</string> <string name="please_enter_search_term">Please input a search term.</string>
<string name="please_enter_javaScript">Please input a validation script.</string> <string name="please_enter_javaScript">Please input a validation script.</string>
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string> <string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="remove_site">Remove Site</string> <string name="remove_site">Remove Site</string>

View file

@ -21,7 +21,12 @@ import android.widget.EditText
import androidx.annotation.IntRange import androidx.annotation.IntRange
import kotlin.math.min import kotlin.math.min
fun EditText.setTextAndMaintainSelection(text: CharSequence) { fun EditText.setTextAndMaintainSelection(text: CharSequence?) {
if (text == null) {
setText("")
return
}
val formerStart = min(selectionStart, text.length) val formerStart = min(selectionStart, text.length)
val formerEnd = min(selectionEnd, text.length) val formerEnd = min(selectionEnd, text.length)
setText(text) setText(text)