From 3df03bbb1ef51f096e0a4a2c527fae9a5701f05e Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sun, 10 May 2015 23:22:35 +1000
Subject: [PATCH] Fix #250. Fix #251. Normalize URLs before saving, and
 disallow invalid URLs.

Removes trailing slashes from URLs, replaces multiple consecutive forward
slashes in the path with a single slash. Canonicalizes the URL.

If the URL is invalid, display a message to the user and don't let it get
added.

NOTE: This does *not* normalize existing URLs in the database.
---
 F-Droid/res/values/strings.xml                |  1 +
 .../fdroid/views/ManageReposActivity.java     | 51 ++++++++++++++++++-
 2 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index c362c228e..a5269a228 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -97,6 +97,7 @@
 	<string name="repo_exists_enable">This repo is already setup, confirm that you want to re-enable it.</string>
 	<string name="repo_exists_and_enabled">The incoming repo is already setup and enabled.</string>
 	<string name="repo_delete_to_overwrite">You must first delete this repo before you can add one with a different key.</string>
+	<string name="invalid_url">This is not a valid URL.</string>
 	<string name="malformed_repo_uri">Ignoring malformed repo URI: %s</string>
 
 	<string name="repo_alrt">The list of used repositories has
diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
index 15912d2d1..166fbee5c 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -78,6 +78,8 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
 
 import java.io.IOException;
 import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.Date;
 import java.util.Locale;
@@ -98,7 +100,7 @@ public class ManageReposActivity extends ActionBarActivity {
 
     private enum AddRepoState {
         DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_FINGERPRINT_MATCH,
-        EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED
+        EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL
     }
 
     private UpdateService.UpdateReceiver updateHandler = null;
@@ -390,6 +392,13 @@ public class ManageReposActivity extends ActionBarActivity {
                         String fp = fingerprintEditText.getText().toString();
                         String url = uriEditText.getText().toString();
 
+                        try {
+                            url = normalizeUrl(url);
+                        } catch (URISyntaxException e) {
+                            invalidUrl();
+                            return;
+                        }
+
                         switch(addRepoState) {
                             case DOESNT_EXIST:
                                 prepareToCreateNewRepo(url, fp);
@@ -449,7 +458,15 @@ public class ManageReposActivity extends ActionBarActivity {
          * Compare the repo and the fingerprint against existing repositories, to see if this
          * repo matches and display a relevant message to the user if that is the case.
          */
-        private void validateRepoDetails(@NonNull final String uri, @NonNull String fingerprint) {
+        private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
+
+            try {
+                uri = normalizeUrl(uri);
+            } catch (URISyntaxException e) {
+                // Don't bother dealing with this exception yet, as this is called every time
+                // a letter is added to the repo URL text input. We don't want to display a message
+                // to the user until they try to save the repo.
+            }
 
             final Repo repo = uri.length() > 0 ? RepoProvider.Helper.findByAddress(context, uri) : null;
 
@@ -485,6 +502,11 @@ public class ManageReposActivity extends ActionBarActivity {
                     true, R.string.overwrite, false);
         }
 
+        private void invalidUrl() {
+            updateUi(AddRepoState.INVALID_URL, R.string.invalid_url, true,
+                    R.string.repo_add_add, false);
+        }
+
         private void repoExistsAndDisabled() {
             updateUi(AddRepoState.EXISTS_DISABLED,
                     R.string.repo_exists_enable, false, R.string.enable, true);
@@ -606,6 +628,31 @@ public class ManageReposActivity extends ActionBarActivity {
             checker.execute(originalAddress);
         }
 
+        /**
+         * Some basic sanitization of URLs, so that two URLs which have the same semantic meaning
+         * are represented by the exact same string by F-Droid. This will help to make sure that,
+         * e.g. "http://10.0.1.50" and "http://10.0.1.50/" are not two different repositories.
+         *
+         * Currently it normalizes the path so that "/./" are removed and "test/../" is collapsed.
+         * This is done using {@link URI#normalize()}. It also removes multiple consecutive forward
+         * slashes in the path and replaces them with one. Finally, it removes trailing slashes.
+         */
+        private String normalizeUrl(String urlString) throws URISyntaxException {
+            URI uri = new URI(urlString);
+            if (!uri.isAbsolute()) {
+                throw new URISyntaxException(urlString, "Must provide an absolute URI for repositories");
+            }
+
+            uri = uri.normalize();
+            String path = uri.getPath().replaceAll("//*/", "/"); // Collapse multiple forward slashes into 1.
+            if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
+                path = path.substring(0, path.length() - 1);
+            }
+
+            return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
+                    path, uri.getQuery(), uri.getFragment()).toString();
+        }
+
         private void createNewRepo(String address, String fingerprint) {
             ContentValues values = new ContentValues(2);
             values.put(RepoProvider.DataColumns.ADDRESS, address);