Commit 29f57a8a authored by Christophe Henry's avatar Christophe Henry

Fix #57: http:// scheme is reduced to http:/

parent 5a249c76
...@@ -22,7 +22,6 @@ android { ...@@ -22,7 +22,6 @@ android {
} }
} }
} }
sourceSets { sourceSets {
androidTest.assets.srcDirs += files(schema_location) androidTest.assets.srcDirs += files(schema_location)
} }
......
...@@ -8,24 +8,24 @@ import android.os.Bundle ...@@ -8,24 +8,24 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.*
import android.widget.AdapterView.OnItemSelectedListener
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
import fr.chenry.android.freshrss.store.Store import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.Endpoints
import fr.chenry.android.freshrss.utils.* import fr.chenry.android.freshrss.utils.*
import kotlinx.android.synthetic.main.activity_login.* import kotlinx.android.synthetic.main.activity_login.*
import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import java.util.* import java.util.Properties
/** /**
* A login screen that offers login via email/password. * A login screen that offers login via email/password.
*/ */
class LoginActivity: AppCompatActivity() { class LoginActivity: AppCompatActivity() {
private lateinit var instanceUrl: InstanceUrl
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -40,18 +40,28 @@ class LoginActivity: AppCompatActivity() { ...@@ -40,18 +40,28 @@ class LoginActivity: AppCompatActivity() {
}) })
email_sign_in_button.setOnClickListener {attemptLogin()} email_sign_in_button.setOnClickListener {attemptLogin()}
instanceUrl = InstanceUrl(InstanceUrl.authorizedProtocols[0], resources.getString(R.string.instance_exemple))
activity_login_protocol_selection.adapter =
ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, InstanceUrl.authorizedProtocols)
try { Try {
val properties = Properties() val properties = Properties()
properties.load(baseContext.assets.open("config.properties")) properties.load(baseContext.assets.open("config.properties"))
instance.setText(properties.getProperty("instance")) instanceUrl = InstanceUrl.parse(properties.getProperty("instance"))
instance.setText(instanceUrl.base)
val protocolIdx = InstanceUrl.authorizedProtocols.indexOf(instanceUrl.protocole)
activity_login_protocol_selection.setSelection(if(protocolIdx >= 0) protocolIdx else 0)
login.setText(properties.getProperty("login")) login.setText(properties.getProperty("login"))
password.setText(properties.getProperty("password")) password.setText(properties.getProperty("password"))
} catch(_: Throwable) {
} }
computeInstanceURLHint() computeInstanceURLHint()
instance.addTextChangedListener {computeInstanceURLHint()} instance.addTextChangedListener {computeInstanceURLHint()}
activity_login_protocol_selection.onItemSelectedListener = object: OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = computeInstanceURLHint()
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) =
computeInstanceURLHint()
}
resetErrors() resetErrors()
instance.requestFocus() instance.requestFocus()
} }
...@@ -59,14 +69,19 @@ class LoginActivity: AppCompatActivity() { ...@@ -59,14 +69,19 @@ class LoginActivity: AppCompatActivity() {
private fun attemptLogin() { private fun attemptLogin() {
resetErrors() resetErrors()
var cancel = false
var focusView: View? = null
if(instanceUrl.isMalformed) {
instance.error = getString(R.string.instance_url_malformed)
focusView = instance
cancel = true
}
// Store values at the time of the login attempt. // Store values at the time of the login attempt.
val instanceStr = instance.text.toString()
val loginStr = login.text.toString() val loginStr = login.text.toString()
val passwordStr = password.text.toString() val passwordStr = password.text.toString()
var cancel = false
var focusView: View? = null
// Check for a valid password, if the user entered one. // Check for a valid password, if the user entered one.
if(passwordStr.isBlank()) { if(passwordStr.isBlank()) {
password.error = getString(R.string.error_invalid_password) password.error = getString(R.string.error_invalid_password)
...@@ -85,7 +100,7 @@ class LoginActivity: AppCompatActivity() { ...@@ -85,7 +100,7 @@ class LoginActivity: AppCompatActivity() {
focusView?.requestFocus() focusView?.requestFocus()
} else { } else {
showProgress(true) showProgress(true)
Store.login(instanceStr, loginStr, passwordStr) successUi { Store.login(instanceUrl.toString(), loginStr, passwordStr) successUi {
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} failUi { } failUi {
...@@ -100,21 +115,10 @@ class LoginActivity: AppCompatActivity() { ...@@ -100,21 +115,10 @@ class LoginActivity: AppCompatActivity() {
val instanceStr = instance.text.toString().let { val instanceStr = instance.text.toString().let {
if(it.isBlank()) resources.getString(R.string.instance_exemple) else it if(it.isBlank()) resources.getString(R.string.instance_exemple) else it
} }
reified_instance_url.text = instanceStr.cleanUrlSlashes().replace("/$".toRegex(), "")
if(!Endpoints.hasProtocol(instanceStr)) {
protocol_default.text = Endpoints.defaultProtocol
protocol_default.visibility = View.VISIBLE
} else {
protocol_default.visibility = View.GONE
}
if(!Endpoints.hasApiEndpoint(instanceStr)) { val protocole = activity_login_protocol_selection.selectedItem.toString()
api_endpoint.text = Endpoints.apiEndpoint val instanceUrl = InstanceUrl(protocole, instanceStr)
api_endpoint.visibility = View.VISIBLE reified_instance_url.text = instanceUrl.toSpanString()
} else {
api_endpoint.visibility = View.GONE
}
} }
private fun resetErrors() { private fun resetErrors() {
...@@ -134,10 +138,7 @@ class LoginActivity: AppCompatActivity() { ...@@ -134,10 +138,7 @@ class LoginActivity: AppCompatActivity() {
} }
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {
login_progress_text.text = resources.getString( login_progress_text.text = resources.getString(R.string.login_progress_text, instanceUrl.toString())
R.string.login_progress_text,
Endpoints.computeApiUrl(instance.text.toString())
)
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
......
package fr.chenry.android.freshrss.store.api package fr.chenry.android.freshrss.store.api
import fr.chenry.android.freshrss.utils.cleanUrlSlashes class Endpoints(private val base: String) {
class Endpoints(base: String) {
private val base = computeApiUrl(base)
val loginEndpoint = "${this.base}/accounts/ClientLogin" val loginEndpoint = "${this.base}/accounts/ClientLogin"
val tokenEndpoint = "${this.base}/reader/api/0/token" val tokenEndpoint = "${this.base}/reader/api/0/token"
val subscriptionEndpoint = "${this.base}/reader/api/0/subscription/list" val subscriptionEndpoint = "${this.base}/reader/api/0/subscription/list"
...@@ -12,21 +9,4 @@ class Endpoints(base: String) { ...@@ -12,21 +9,4 @@ class Endpoints(base: String) {
val tagEndpoint = "${this.base}/reader/api/0/tag/list" val tagEndpoint = "${this.base}/reader/api/0/tag/list"
val unreadItemsEndpoint = "${this.base}/reader/api/0/stream/contents/user/-/state/com.google/reading-list" val unreadItemsEndpoint = "${this.base}/reader/api/0/stream/contents/user/-/state/com.google/reading-list"
val editTagEnpoint = "${this.base}/reader/api/0/edit-tag" val editTagEnpoint = "${this.base}/reader/api/0/edit-tag"
companion object {
const val defaultProtocol = "https://"
const val apiEndpoint = "/api/greader.php"
fun computeApiUrl(fromUrl: String): String {
return when(hasProtocol(fromUrl) to hasApiEndpoint(fromUrl)) {
(false to false) -> "$defaultProtocol${"$fromUrl$apiEndpoint".cleanUrlSlashes()}"
(false to true) -> "$defaultProtocol${fromUrl.cleanUrlSlashes()}"
(true to false) -> "$fromUrl$apiEndpoint"
else -> fromUrl
}
}
fun hasProtocol(url: String) = url.matches("^https?://.*$".toRegex())
fun hasApiEndpoint(url: String) = url.endsWith(apiEndpoint)
}
} }
\ No newline at end of file
package fr.chenry.android.freshrss.utils
import android.text.SpannableStringBuilder
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.R
import java.net.URL
class InstanceUrl(val protocole: String, base: String) {
val isMalformed: Boolean
val base = base
.replace(".*://".toRegex(), "")
.replace("//+".toRegex(), "/")
.replace("/$".toRegex(), "")
private val hasApiEndpoint: Boolean = this.base.endsWith(apiEndpoint)
private val endpoint: String = if(hasApiEndpoint) "" else apiEndpoint
init {
isMalformed = Try {URL("$protocole${this.base}")}.getOrNull() == null
}
fun toSpanString(): CharSequence = SpannableStringBuilder().apply {
val urlString = base.let {if(hasApiEndpoint) it else it.replace(apiEndpoint, "")}
color(ContextCompat.getColor(FreshRSSApplication.context, R.color.grey)) {append(protocole)}
bold {append(urlString)}
if(!hasApiEndpoint)
color(ContextCompat.getColor(FreshRSSApplication.context, R.color.grey)) {append(apiEndpoint)}
}
override fun toString(): String = "$protocole$base$endpoint"
companion object {
private const val apiEndpoint = "/api/greader.php"
val authorizedProtocols = arrayOf("https://", "http://")
fun parse(url: String): InstanceUrl {
"(.*://)".toPattern().matcher(url).let {
if(it.find()) {
val protocol = it.group(0)
if(authorizedProtocols.contains(protocol)) return InstanceUrl(protocol, url)
}
}
return InstanceUrl(authorizedProtocols[0], url)
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
...@@ -24,53 +24,51 @@ ...@@ -24,53 +24,51 @@
android:visibility="visible" android:visibility="visible"
> >
<com.google.android.material.textfield.TextInputLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="horizontal"
<EditText >
android:id="@+id/instance" <Spinner
android:layout_width="match_parent" android:id="@+id/activity_login_protocol_selection"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:entries="@array/connection_options"
android:dropDownWidth="wrap_content"
android:spinnerMode="dropdown" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/prompt_instance" android:layout_weight="100" >
android:maxLines="1" <EditText
android:singleLine="true" android:id="@+id/instance"
android:inputType="textUri" android:layout_width="match_parent"
android:autoLink="web|none" /> android:layout_height="wrap_content"
</com.google.android.material.textfield.TextInputLayout> android:hint="@string/prompt_instance"
android:maxLines="1"
android:singleLine="true"
android:inputType="textUri"
android:autoLink="web|none" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:paddingLeft="6dp" android:paddingLeft="6dp"
android:paddingRight="6dp" android:paddingRight="6dp"
android:layout_marginBottom="@dimen/activity_horizontal_margin"> android:paddingTop="@dimen/subscription_section_h_padding"
android:paddingBottom="@dimen/subscription_section_h_padding"
<TextView android:orientation="horizontal"
android:id="@+id/protocol_default" >
android:text="@string/protocol_default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/grey"
/>
<TextView <TextView
android:id="@+id/reified_instance_url" android:id="@+id/reified_instance_url"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/instance_exemple" android:text="@string/instance_exemple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
/>
<TextView
android:id="@+id/api_endpoint"
android:text="@string/api_endpoint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/grey"
/> />
</LinearLayout> </LinearLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
......
...@@ -20,13 +20,13 @@ ...@@ -20,13 +20,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/share" android:text="@string/share"
android:layout_margin="16dp" android:layout_margin="16dp"
app:icon="?attr/actionModeShareDrawable" /> app:icon="?attr/actionModeShareDrawable" android:theme="@style/FabAppTheme" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_open_browser" android:id="@+id/fab_open_browser"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:text="@string/original_page" android:text="@string/original_page"
app:icon="@drawable/ic_web_black_24dp" /> app:icon="@drawable/ic_web_black_24dp" android:theme="@style/FabAppTheme" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
<resources> <resources>
<!-- Do not translate --> <!-- Do not translate -->
<string name="app_name">FreshRSS</string> <string name="app_name">FreshRSS</string>
<string-array name="connection_options">
<item>https://</item>
<item>http://</item>
</string-array>
<string name="prompt_login">Login</string> <string name="prompt_login">Login</string>
<string name="prompt_password">Password</string> <string name="prompt_password">Password</string>
...@@ -74,4 +78,5 @@ ...@@ -74,4 +78,5 @@
<string name="subscription_categories">Categories</string> <string name="subscription_categories">Categories</string>
<string name="share_article">Share article of %s</string> <string name="share_article">Share article of %s</string>
<string name="this_feed">this feed</string> <string name="this_feed">this feed</string>
<string name="instance_url_malformed">This URL is malformed</string>
</resources> </resources>
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/color_primary</item> <item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/color_primary_dark</item> <item name="colorPrimaryDark">@color/color_primary_dark</item>
...@@ -11,11 +11,18 @@ ...@@ -11,11 +11,18 @@
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
</style> </style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" /> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" /> <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="AppTheme.ExtendedFloatingActionButton" parent="Widget.MaterialComponents.ExtendedFloatingActionButton"> <style name="AppTheme.ExtendedFloatingActionButton" parent="Widget.MaterialComponents.ExtendedFloatingActionButton">
<item name="backgroundTint">@color/color_primary</item> <item name="backgroundTint">@color/color_primary</item>
<item name="android:textColor">@color/design_default_color_on_primary</item> <item name="android:textColor">@color/design_default_color_on_primary</item>
<item name="iconTint">@color/design_default_color_on_primary</item> <item name="iconTint">@color/design_default_color_on_primary</item>
</style> </style>
<style name="FabAppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/color_primary_dark</item>
<item name="colorAccent">@color/color_accent</item>
<item name="extendedFloatingActionButtonStyle">@style/AppTheme.ExtendedFloatingActionButton</item>
</style>
</resources> </resources>
package fr.chenry.android.freshrss.store.api
import fr.chenry.android.freshrss.utils.InstanceUrl
import fr.chenry.android.freshrss.utils.InstanceUrl.Companion
import org.junit.*
import org.junit.Assert.*
class InstanceUrlTest {
@Test
fun testComputeApiUrl() {
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com").toString())
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com/").toString())
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com//").toString())
assertEquals("https://your-instance.com/p/api/greader.php", InstanceUrl.parse("your-instance.com/p").toString())
assertEquals("https://your-instance.com/p/api/greader.php", InstanceUrl.parse("your-instance.com/p/").toString())
assertEquals("https://your-instance.com/p/api/greader.php", InstanceUrl.parse("your-instance.com/p//").toString())
assertEquals("https://your-instance.com/p/api/greader.php", InstanceUrl.parse("your-instance.com//p//").toString())
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com/api/greader.php").toString())
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com/api/greader.php/").toString())
assertEquals("https://your-instance.com/api/greader.php", InstanceUrl.parse("your-instance.com//api/greader.php/").toString())
assertEquals("https://your-instance.com/p/api/greader.php", InstanceUrl.parse("your-instance.com/p/api/greader.php").toString())
assertEquals("http://your-instance.com/p/api/greader.php", InstanceUrl.parse("http://your-instance.com/p/api/greader.php").toString())
assertEquals("http://your-instance.com/p/api/greader.php", InstanceUrl.parse("http://your-instance.com//p/api/greader.php").toString())
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment