Commit d4f3a477 authored by Bartek Fabiszewski's avatar Bartek Fabiszewski
Browse files

Relax URL validation (closes #16), do validation before closing preference dialog

parent 10b44ab5
Loading
Loading
Loading
Loading
+0 −25
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.util.Patterns;
import android.widget.Toast;

/**
@@ -44,32 +43,12 @@ public class SettingsActivity extends PreferenceActivity {
    @SuppressWarnings("deprecation")
    private void onCreatePreferenceActivity() {
        addPreferencesFromResource(R.xml.preferences);
        final Preference prefHost = findPreference("prefHost");
        if (prefHost != null) {
            prefHost.setOnPreferenceChangeListener(hostChanged);
        }
        final Preference prefLiveSync = findPreference("prefLiveSync");
        if (prefLiveSync != null) {
            prefLiveSync.setOnPreferenceChangeListener(liveSyncChanged);
        }
    }

    /**
     * On change listener to validate server url
     */
    private final static Preference.OnPreferenceChangeListener hostChanged = new Preference.OnPreferenceChangeListener() {
        @Override
        public boolean onPreferenceChange(Preference preference, Object newValue) {
            if (Patterns.WEB_URL.matcher(newValue.toString().trim()).matches()) {
                return true;
            } else {
                Toast.makeText(preference.getContext(), R.string.provide_valid_url, Toast.LENGTH_LONG).show();
                return false;
            }
        }

    };

    /**
     * On change listener to validate whether live synchronization is allowed
     */
@@ -114,10 +93,6 @@ public class SettingsActivity extends PreferenceActivity {
        public void onCreate(final Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.preferences);
            final Preference prefHost = findPreference("prefHost");
            if (prefHost != null) {
                prefHost.setOnPreferenceChangeListener(hostChanged);
            }
            final Preference prefLiveSync = findPreference("prefLiveSync");
            if (prefLiveSync != null) {
                prefLiveSync.setOnPreferenceChangeListener(liveSyncChanged);
+74 −0
Original line number Diff line number Diff line
@@ -10,32 +10,36 @@
package net.fabiszewski.ulogger;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.support.annotation.RequiresApi;
import android.app.AlertDialog;
import android.util.AttributeSet;
import android.view.View;

/**
 * Trimmed edit text preference
 *
 * URL edit text preference
 * Validates and trims URL
 */

class TrimmedEditTextPreference extends EditTextPreference {
class UrlEditTextPreference extends EditTextPreference {

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TrimmedEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    public UrlEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public TrimmedEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
    public UrlEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public TrimmedEditTextPreference(Context context, AttributeSet attrs) {
    public UrlEditTextPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TrimmedEditTextPreference(Context context) {
    public UrlEditTextPreference(Context context) {
        super(context);
    }

@@ -43,4 +47,28 @@ class TrimmedEditTextPreference extends EditTextPreference {
    public void setText(String text) {
        super.setText(text.trim());
    }

    @Override
    protected void showDialog(Bundle state) {
        super.showDialog(state);
        getEditText().setError(null);
        final AlertDialog dialog = (AlertDialog) getDialog();
        View positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
        positiveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onPositiveButtonClicked();
            }
        });
    }

    private void onPositiveButtonClicked() {
        if (WebHelper.isValidURL(getEditText().getText().toString().trim())) {
            getEditText().setError(null);
            onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
            getDialog().dismiss();
        } else {
            getEditText().setError(getContext().getString(R.string.provide_valid_url));
        }
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -297,4 +297,14 @@ class WebHelper {
        }
    }

    /**
     * Check whether given url is valid.
     * Uses relaxed pattern (@see WebPatterns#WEB_URL_RELAXED)
     * @param url URL
     * @return True if valid, false otherwise
     */
    static boolean isValidURL(String url) {
        return WebPatterns.WEB_URL_RELAXED.matcher(url).matches();
    }

}
+96 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2017 Bartek Fabiszewski
 * http://www.fabiszewski.net
 *
 * This file is part of μlogger-android.
 * Licensed under GPL, either version 3, or any later.
 * See <http://www.gnu.org/licenses/>
 */

package net.fabiszewski.ulogger;

import java.util.regex.Pattern;

/**
 * This is based on java.android.util.Patterns
 * with WEB_URL pattern relaxed
 */

class WebPatterns {

    /**
     * Protocols limited to http(s).
     */
    private static final String PROTOCOL = "(?i:https?)://";

    private static final String USER_INFO = "(?:[-a-zA-Z0-9$_.+!*'(),;?&=]|(?:%[a-fA-F0-9]{2})){1,64}"
            + "(?::(?:[-a-zA-Z0-9$_.+!*'(),;?&=]|(?:%[a-fA-F0-9]{2})){1,25})?@";

    /**
     * Valid UCS characters defined in RFC 3987. Excludes space characters.
     */
    private static final String UCS_CHAR = "[" +
            "\u00A0-\uD7FF" +
            "\uF900-\uFDCF" +
            "\uFDF0-\uFFEF" +
            "\uD800\uDC00-\uD83F\uDFFD" +
            "\uD840\uDC00-\uD87F\uDFFD" +
            "\uD880\uDC00-\uD8BF\uDFFD" +
            "\uD8C0\uDC00-\uD8FF\uDFFD" +
            "\uD900\uDC00-\uD93F\uDFFD" +
            "\uD940\uDC00-\uD97F\uDFFD" +
            "\uD980\uDC00-\uD9BF\uDFFD" +
            "\uD9C0\uDC00-\uD9FF\uDFFD" +
            "\uDA00\uDC00-\uDA3F\uDFFD" +
            "\uDA40\uDC00-\uDA7F\uDFFD" +
            "\uDA80\uDC00-\uDABF\uDFFD" +
            "\uDAC0\uDC00-\uDAFF\uDFFD" +
            "\uDB00\uDC00-\uDB3F\uDFFD" +
            "\uDB44\uDC00-\uDB7F\uDFFD" +
            "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";

    /**
     * Valid characters for IRI label defined in RFC 3987.
     */
    private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;

    /**
     * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
     */
    private static final String IRI_LABEL = "[" + LABEL_CHAR + "]"
            + "(?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "])?";

    /**
     * Regular expression that matches domain names without a TLD, also IP addresses
     */
    private static final String RELAXED_DOMAIN_NAME =
            "(?:(?:" + IRI_LABEL + "(?:\\.(?=[" + LABEL_CHAR + "]))?)+)";

    private static final String PORT_NUMBER = ":\\d{1,5}";

    /**
     * A word boundary or end of input. This is to stop foo.sure from matching as foo.su
     */
    private static final String WORD_BOUNDARY = "(?:\\b|$|^)";

    /**
     * Path segment, exclude repeated slashes to rule out common error (http//example.com)
     */
    private static final String PATH_SEGMENT =
            "/(?:(?:[" + LABEL_CHAR + ";:@&=~\\-.+!*'(),_])|(?:%[a-fA-F0-9]{2})|" + WORD_BOUNDARY + ")+";

    /**
     * Regular expression pattern to match most part of RFC 3987
     * Internationalized URLs, aka IRIs.
     * Relaxed to accept domains without a TLD.
     * Will not accept query part.
     * Only http and https protocols.
     */
    static final Pattern WEB_URL_RELAXED = Pattern.compile(
            "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
                    + RELAXED_DOMAIN_NAME
                    + "(?:" + PORT_NUMBER + ")?"
                    + "(?:" + PATH_SEGMENT + ")*"
                    + WORD_BOUNDARY);

}
 No newline at end of file
+2 −2
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@
  -->

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <net.fabiszewski.ulogger.TrimmedEditTextPreference
    <net.fabiszewski.ulogger.UrlEditTextPreference
        android:key="prefUsername"
        android:title="@string/pref_username_title"
        android:summary="@string/pref_username_summary"
@@ -19,7 +19,7 @@
        android:title="@string/pref_pass_title"
        android:summary="@string/pref_pass_summary"
        android:inputType="textPassword" />
    <net.fabiszewski.ulogger.TrimmedEditTextPreference
    <net.fabiszewski.ulogger.UrlEditTextPreference
        android:key="prefHost"
        android:title="@string/pref_host_title"
        android:summary="@string/pref_host_summary"
Loading