diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 9d471c664b82..ab3f74209745 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -1647,7 +1647,9 @@ defmodule PlausibleWeb.Api.StatsController do _opts ) when is_integer(segment_id) do - case ensure_expected_segment_filter_present(conn.params, segment_id) do + case conn.params + |> get_filters_param() + |> ensure_expected_segment_filter_present(segment_id) do :ok -> conn @@ -1661,11 +1663,24 @@ defmodule PlausibleWeb.Api.StatsController do defp validate_required_filters_plug(conn, _opts), do: conn + defp get_filters_param(%{"filters" => filters} = _params) when is_binary(filters) do + JSON.decode!(filters) + end + + defp get_filters_param(%{"filters" => filters} = _params) when is_list(filters) do + filters + end + + defp get_filters_param(_params) do + nil + end + defp ensure_expected_segment_filter_present( - %{"filters" => filters} = _params, + filters, expected_segment_id - ) do - case JSON.decode!(filters) do + ) + when is_list(filters) do + case filters do [["is", "segment", [segment_id]] | _other_filters] when segment_id == expected_segment_id -> :ok @@ -1674,7 +1689,7 @@ defmodule PlausibleWeb.Api.StatsController do end end - defp ensure_expected_segment_filter_present(_params, _expected_segment_id) do + defp ensure_expected_segment_filter_present(_filters, _expected_segment_id) do :error end diff --git a/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs new file mode 100644 index 000000000000..d7bab5c17d9b --- /dev/null +++ b/test/plausible_web/controllers/api/stats_controller/query_controller_test.exs @@ -0,0 +1,137 @@ +defmodule PlausibleWeb.Api.InternalController.QueryTest do + use PlausibleWeb.ConnCase, async: false + use Plausible.Repo + + describe "POST /api/:domain/query shared/public" do + test "returns aggregated metrics for public site", %{conn: conn} do + site = new_site(public: true) + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]) + ]) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "event:page", ["/foo"]]] + }) + + assert json_response(conn, 200)["results"] == [ + %{"metrics" => [1], "dimensions" => []} + ] + end + + test "returns aggregated metrics with shared link auth", %{conn: conn} do + site = new_site() + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + link = insert(:shared_link, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "event:page", ["/foo"]]] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [1], "dimensions" => []}] + end + + test "returns metrics with shared link auth that is limited to segment", %{conn: conn} do + site = new_site() + + populate_stats(site, [ + build(:pageview, user_id: 5, pathname: "/foo", timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + segment = + insert(:segment, + site: site, + name: "Scandinavia", + type: :site, + segment_data: %{"filters" => [["is", "event:page", ["/foo"]]]} + ) + + link = insert(:shared_link, segment_id: segment.id, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [["is", "segment", [segment.id]]] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [1], "dimensions" => []}] + end + + test "errors when expected segment filter not present for shared link auth that is limited to segment", + %{conn: conn} do + site = new_site() + + segment = + insert(:segment, + site: site, + name: "Scandinavia", + type: :site, + segment_data: %{"filters" => [["is", "event:page", ["/foo"]]]} + ) + + link = insert(:shared_link, segment_id: segment.id, site: site) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?auth=#{link.slug}", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 400) == %{ + "error" => "The first filter must be for the segment with id #{segment.id}" + } + end + end + + describe "POST /api/:domain/query" do + setup [:create_user, :create_site, :log_in] + + test "rejects when accessing any other site", %{conn: conn, site: site} do + conn = + post(conn, "/api/stats/any.other.site/query", %{ + # ignored + "site_id" => site.domain, + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 404) == %{ + "error" => "Site does not exist or user does not have sufficient access." + } + end + + test "returns aggregated metrics", %{conn: conn, site: site} do + populate_stats(site, [ + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, user_id: 5, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]) + ]) + + conn = + post(conn, "/api/stats/#{URI.encode(site.domain)}/query?site_id=ignored", %{ + "metrics" => ["pageviews"], + "date_range" => "all", + "filters" => [] + }) + + assert json_response(conn, 200)["results"] == [%{"metrics" => [3], "dimensions" => []}] + end + end +end