Repository: google/gae-secure-scaffold-python3

Issue:
Preserve query params on redirect
## Issue
Currently, any query string set on the redirect path is not preserved when redirected.

## Behaviour

**Current**: `/?foo=bar` -> `/intl/en/`
**Expected**: `/?foo=bar` -> `/intl/en/?foo=bar`

## Setup
```py
# Follows github.com/google/gae-secure-scaffold-python3/blob/master/examples/language-redirect/main.py

app = securescaffold.create_app(__name__)
app.add_url_rule("/", "lang_redirect", securescaffold.views.lang_redirect)

app.config["LOCALES"] = ["en"]
app.config["LOCALES_REDIRECT_TO"] = "/intl/{locale}/"
```


Discussion:
davidwtbuxton: Hmmm… to support redirecting with the path would be quite different. Let's get this merged first, then do a follow-up change request as you suggested.

Patch:
diff --git a/src/securescaffold/views.py b/src/securescaffold/views.py
index b53c0aa..5de1852 100644
--- a/src/securescaffold/views.py
+++ b/src/securescaffold/views.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import urllib.parse
 from typing import Optional
 
 import flask
@@ -44,6 +45,17 @@ def best_match(requested_langs: LanguageAccept, supported_langs: list) -> Option
     return result
 
 
+def add_query_to_url(path: str, qs: str) -> str:
+    """Add query params to a URL path, preserving existing path params."""
+    parsed = urllib.parse.urlsplit(path)
+    old_params = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
+    new_params = urllib.parse.parse_qsl(qs, keep_blank_values=True)
+    query = urllib.parse.urlencode(old_params + new_params)
+    parsed = parsed._replace(query=query)
+
+    return urllib.parse.urlunsplit(parsed)
+
+
 def lang_redirect():
     """Redirects the user depending on the Accept-Language header.
 
@@ -63,4 +75,8 @@ def lang_redirect():
 
     redirect_to = locales_redirect_to.format(locale=locale)
 
+    if flask.request.query_string:
+        # Preserve query parameters on redirect.
+        redirect_to = add_query_to_url(redirect_to, flask.request.query_string)
+
     return flask.redirect(redirect_to)


Test patch:
diff --git a/src/securescaffold/tests/test_views.py b/src/securescaffold/tests/test_views.py
index 96ab8e0..92b5d4c 100644
--- a/src/securescaffold/tests/test_views.py
+++ b/src/securescaffold/tests/test_views.py
@@ -75,3 +75,18 @@ class RedirectTestCase(unittest.TestCase):
 
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.location, "/intl/en/")
+
+    def test_redirect_with_query(self):
+        client = demo_app.test_client()
+        response = client.get("/", query_string={"foo": "bar"})
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.location, "/intl/en/?foo=bar")
+
+    def test_redirect_with_query_preserves_original_query(self):
+        client = demo_app.test_client()
+        demo_app.config["LOCALES_REDIRECT_TO"] = "/test?baz=qux"
+        response = client.get("/", query_string={"foo": "bar"})
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response.location, "/test?baz=qux&foo=bar")