Skip to content

Commit 13beb42

Browse files
committed
WIP
1 parent e4b282a commit 13beb42

File tree

6 files changed

+139
-29
lines changed

6 files changed

+139
-29
lines changed

lib/plausible/stats/dashboard_query_serializer.ex

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ defmodule Plausible.Stats.DashboardQuerySerializer do
5151
defp get_serialized_fields({:filters, [_ | _] = filters}) do
5252
filters
5353
|> Enum.map(fn [operator, dimension, clauses] ->
54-
clauses = Enum.map_join(clauses, ",", &uri_encode_permissive/1)
54+
clauses = Enum.map_join(clauses, ",", &PlausibleWeb.URIEncoding.uri_encode_permissive/1)
5555
dimension = String.split(dimension, ":", parts: 2) |> List.last()
5656
{"f", "#{operator},#{dimension},#{clauses}"}
5757
end)
@@ -68,18 +68,4 @@ defmodule Plausible.Stats.DashboardQuerySerializer do
6868
defp get_serialized_fields(_) do
6969
[]
7070
end
71-
72-
# These characters are not URL encoded to have more readable URLs.
73-
# Browsers seem to handle this just fine. `?f=is,page,/my/page/:some_param`
74-
# vs `?f=is,page,%2Fmy%2Fpage%2F%3Asome_param`
75-
@do_not_url_encode [":", "/"]
76-
@do_not_url_encode_map Enum.into(@do_not_url_encode, %{}, fn char ->
77-
{URI.encode_www_form(char), char}
78-
end)
79-
80-
defp uri_encode_permissive(input) do
81-
input
82-
|> URI.encode_www_form()
83-
|> String.replace(Map.keys(@do_not_url_encode_map), &@do_not_url_encode_map[&1])
84-
end
8571
end

lib/plausible_web/controllers/stats_controller.ex

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ defmodule PlausibleWeb.StatsController do
282282
)
283283

284284
if shared_link do
285-
new_link_format = Routes.stats_path(conn, :shared_link, shared_link.site.domain, auth: slug)
285+
new_link_format =
286+
Routes.stats_path(conn, :shared_link, shared_link.site.domain, [], auth: slug)
287+
286288
redirect(conn, to: new_link_format)
287289
else
288290
render_error(conn, 404)
@@ -306,6 +308,15 @@ defmodule PlausibleWeb.StatsController do
306308
defp render_password_protected_shared_link(conn, shared_link) do
307309
conn = Plug.Conn.fetch_cookies(conn)
308310

311+
star_path = conn.path_params["path"]
312+
313+
star_path_fragment = serialize_star_path_as_query_string_fragment(star_path)
314+
315+
query_string =
316+
[conn.query_string, star_path_fragment]
317+
|> Enum.filter(fn v -> is_binary(v) and String.length(v) > 0 end)
318+
|> Enum.join("&")
319+
309320
case validate_shared_link_password(conn, shared_link) do
310321
{:ok, shared_link} ->
311322
render_shared_link(conn, shared_link)
@@ -314,7 +325,7 @@ defmodule PlausibleWeb.StatsController do
314325
conn
315326
|> render("shared_link_password.html",
316327
link: shared_link,
317-
query_string: conn.query_string,
328+
query_string: query_string,
318329
dogfood_page_path: "/share/:dashboard"
319330
)
320331
end
@@ -349,12 +360,23 @@ defmodule PlausibleWeb.StatsController do
349360
if Plausible.Auth.Password.match?(password, shared_link.password_hash) do
350361
token = Plausible.Auth.Token.sign_shared_link(slug)
351362

363+
star_path = parse_star_path(conn)
364+
365+
query_string_fragment =
366+
get_rest_of_query_string(conn) |> omit_from_query_string("return_to")
367+
352368
conn
353369
|> put_resp_cookie(shared_link_cookie_name(slug), token)
354370
|> redirect(
355371
to:
356-
Routes.stats_path(conn, :shared_link, shared_link.site.domain, auth: slug) <>
357-
qs_appendix(conn)
372+
Routes.stats_path(
373+
conn,
374+
:shared_link,
375+
shared_link.site.domain,
376+
star_path,
377+
auth: slug
378+
) <>
379+
query_string_fragment
358380
)
359381
else
360382
conn
@@ -370,12 +392,47 @@ defmodule PlausibleWeb.StatsController do
370392
end
371393
end
372394

373-
def qs_appendix(conn)
374-
when is_nil(conn.query_string) or
375-
(is_binary(conn.query_string) and byte_size(conn.query_string)) == 0,
376-
do: ""
395+
defp serialize_star_path_as_query_string_fragment(star_path) do
396+
if length(star_path) > 0 do
397+
# make the path start with a /
398+
# to be able to reject values that don't start with a /
399+
serialized_value =
400+
"/#{Enum.join(star_path, "/")}"
401+
|> PlausibleWeb.URIEncoding.uri_encode_permissive()
377402

378-
def qs_appendix(conn), do: "&#{conn.query_string}"
403+
"return_to=#{serialized_value}"
404+
else
405+
nil
406+
end
407+
end
408+
409+
defp parse_star_path(conn) do
410+
return_to_value = conn.query_params["return_to"]
411+
412+
if not is_nil(return_to_value) and String.starts_with?(return_to_value, "/") do
413+
# get rid of the / character that we added
414+
"/" <> trimmed_return_to_value = return_to_value
415+
String.split(trimmed_return_to_value, "/")
416+
else
417+
[]
418+
end
419+
end
420+
421+
defp get_rest_of_query_string(conn)
422+
when is_nil(conn.query_string) or
423+
(is_binary(conn.query_string) and byte_size(conn.query_string)) == 0,
424+
do: ""
425+
426+
defp get_rest_of_query_string(conn), do: "&#{conn.query_string}"
427+
428+
defp omit_from_query_string(query_string, key) do
429+
query_string
430+
|> String.split("&")
431+
|> Enum.reject(fn key_and_value ->
432+
key_and_value == key || String.starts_with?(key_and_value, key)
433+
end)
434+
|> Enum.join("&")
435+
end
379436

380437
defp render_shared_link(conn, shared_link) do
381438
shared_links_feature_access? =

lib/plausible_web/router.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ defmodule PlausibleWeb.Router do
468468
scope "/", PlausibleWeb do
469469
pipe_through [:shared_link]
470470

471-
get "/share/:domain", StatsController, :shared_link
471+
get "/share/:domain/*path", StatsController, :shared_link
472472
post "/share/:slug/authenticate", StatsController, :authenticate_shared_link
473473
end
474474

lib/plausible_web/uri_encoding.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule PlausibleWeb.URIEncoding do
2+
# These characters are not URL encoded to have more readable URLs.
3+
# Browsers seem to handle this just fine. `?f=is,page,/my/page/:some_param`
4+
# vs `?f=is,page,%2Fmy%2Fpage%2F%3Asome_param`
5+
@do_not_url_encode [":", "/"]
6+
@do_not_url_encode_map Enum.into(@do_not_url_encode, %{}, fn char ->
7+
{URI.encode_www_form(char), char}
8+
end)
9+
10+
def uri_encode_permissive(input) do
11+
input
12+
|> URI.encode_www_form()
13+
|> String.replace(Map.keys(@do_not_url_encode_map), &@do_not_url_encode_map[&1])
14+
end
15+
end

test/plausible_web/controllers/stats_controller_test.exs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,7 @@ defmodule PlausibleWeb.StatsControllerTest do
15001500
site_link = insert(:shared_link, site: site, inserted_at: ~N[2021-12-31 00:00:00])
15011501

15021502
conn = get(conn, "/share/#{site_link.slug}")
1503-
assert redirected_to(conn, 302) == "/share/#{site.domain}?auth=#{site_link.slug}"
1503+
assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{site_link.slug}"
15041504
end
15051505

15061506
test "it does nothing for newer links", %{conn: conn} do
@@ -1520,7 +1520,7 @@ defmodule PlausibleWeb.StatsControllerTest do
15201520
insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password"))
15211521

15221522
conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "password"})
1523-
assert redirected_to(conn, 302) == "/share/#{site.domain}?auth=#{link.slug}"
1523+
assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{link.slug}"
15241524

15251525
conn = get(conn, "/share/#{site.domain}?auth=#{link.slug}")
15261526
assert html_response(conn, 200) =~ "stats-react-container"
@@ -1550,7 +1550,7 @@ defmodule PlausibleWeb.StatsControllerTest do
15501550
)
15511551

15521552
conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "password"})
1553-
assert redirected_to(conn, 302) == "/share/#{site.domain}?auth=#{link.slug}"
1553+
assert redirected_to(conn, 302) == "/share/#{site.domain}/?auth=#{link.slug}"
15541554

15551555
conn = get(conn, "/share/#{site2.domain}?auth=#{link2.slug}")
15561556
assert html_response(conn, 200) =~ "Enter password"
@@ -1586,7 +1586,7 @@ defmodule PlausibleWeb.StatsControllerTest do
15861586
)
15871587

15881588
expected_redirect =
1589-
"/share/#{URI.encode_www_form(site.domain)}?auth=#{link.slug}&#{filters}"
1589+
"/share/#{URI.encode_www_form(site.domain)}/?auth=#{link.slug}&#{filters}"
15901590

15911591
assert redirected_to(conn, 302) == expected_redirect
15921592

@@ -1629,6 +1629,42 @@ defmodule PlausibleWeb.StatsControllerTest do
16291629
end
16301630
end
16311631

1632+
test "handles return_to during password authentication", %{conn: conn} do
1633+
site = new_site()
1634+
1635+
link =
1636+
insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password"))
1637+
1638+
filters = "f=is,country,EE&l=EE,Estonia&f=is,browser,Firefox"
1639+
1640+
deep_path = "/filter/source"
1641+
1642+
conn =
1643+
get(
1644+
conn,
1645+
"/share/#{site.domain}#{deep_path}?auth=#{link.slug}&#{filters}"
1646+
)
1647+
1648+
assert html_response(conn, 200) =~ "Enter password"
1649+
html = html_response(conn, 200)
1650+
1651+
expected_action_string = "/share/#{link.slug}/authenticate?auth=#{link.slug}&#{filters}&return_to=#{deep_path}"
1652+
assert html =~ ~s(action="#{expected_action_string |> String.replace("&", "&amp;")}")
1653+
1654+
conn =
1655+
post(
1656+
conn,
1657+
expected_action_string,
1658+
%{password: "password"}
1659+
)
1660+
1661+
expected_redirect =
1662+
"/share/#{URI.encode_www_form(site.domain)}#{deep_path}?auth=#{link.slug}&#{filters}"
1663+
1664+
assert redirected_to(conn, 302) == expected_redirect
1665+
end
1666+
1667+
16321668
describe "dogfood tracking" do
16331669
@describetag :ee_only
16341670

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule PlausibleWeb.URIEncodingTest do
2+
use ExUnit.Case, async: true
3+
4+
for {value, expected} <- [
5+
{"/:dashboard/settings", "/:dashboard/settings"},
6+
{"&", "%26"},
7+
{"=", "%3D"},
8+
{",", "%2C"},
9+
# should be {"hello world", "hello%20world"}, is
10+
{"hello world", "hello+world"},
11+
] do
12+
test "permissively uri-encoding value #{inspect(value)} yields #{inspect(expected)}" do
13+
assert PlausibleWeb.URIEncoding.uri_encode_permissive(unquote(value)) == unquote(expected)
14+
end
15+
end
16+
end

0 commit comments

Comments
 (0)