Skip to content

Commit

Permalink
feat: support custom redirect statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
coladarci committed Apr 10, 2024
1 parent e746795 commit f32b3d2
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 10 deletions.
38 changes: 29 additions & 9 deletions lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -917,40 +917,60 @@ defmodule Phoenix.LiveView do
## Options
* `:to` - the path to redirect to. It must always be a local path
* `:status` - the HTTP status code to use for the redirect. Defaults to 302.
* `:external` - an external path to redirect to. Either a string
or `{scheme, url}` to redirect to a custom scheme
## Examples
{:noreply, redirect(socket, to: "/")}
{:noreply, redirect(socket, to: "/", status: 301)}
{:noreply, redirect(socket, external: "https://example.com")}
"""
def redirect(socket, opts \\ [])
def redirect(socket, opts \\ []) do
cond do
Keyword.has_key?(opts, :to) ->
do_internal_redirect(socket, Keyword.fetch!(opts, :to), Keyword.get(opts, :status))

def redirect(%Socket{} = socket, to: url) do
Keyword.has_key?(opts, :external) ->
do_external_redirect(socket, Keyword.fetch!(opts, :external), Keyword.get(opts, :status))

true ->
raise ArgumentError, "expected :to or :external option in redirect/2"
end
end

defp do_internal_redirect(%Socket{} = socket, url, redirect_status) do
validate_local_url!(url, "redirect/2")
put_redirect(socket, {:redirect, %{to: url}})

put_redirect(socket, {:redirect, add_redirect_status(%{to: url}, redirect_status)})
end

def redirect(%Socket{} = socket, external: url) do
defp do_external_redirect(%Socket{} = socket, url, redirect_status) do
case url do
{scheme, rest} ->
put_redirect(socket, {:redirect, %{external: "#{scheme}:#{rest}"}})
put_redirect(
socket,
{:redirect, add_redirect_status(%{external: "#{scheme}:#{rest}"}, redirect_status)}
)

url when is_binary(url) ->
external_url = Phoenix.LiveView.Utils.valid_string_destination!(url, "redirect/2")
put_redirect(socket, {:redirect, %{external: external_url}})

put_redirect(
socket,
{:redirect, add_redirect_status(%{external: external_url}, redirect_status)}
)

other ->
raise ArgumentError,
"expected :external option in redirect/2 to be valid URL, got: #{inspect(other)}"
end
end

def redirect(%Socket{}, _) do
raise ArgumentError, "expected :to or :external option in redirect/2"
end
defp add_redirect_status(opts, nil), do: opts
defp add_redirect_status(opts, status), do: Map.put(opts, :status, status)

@doc """
Annotates the socket for navigation within the current LiveView.
Expand Down
8 changes: 7 additions & 1 deletion lib/phoenix_live_view/controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ defmodule Phoenix.LiveView.Controller do
)

{:stop, %Socket{redirected: {:redirect, opts}} = socket} ->
redirect_opts = opts |> Map.drop([:status]) |> Map.to_list()

conn
|> put_status(opts)
|> put_flash(LiveView.Utils.get_flash(socket))
|> Phoenix.Controller.redirect(Map.to_list(opts))
|> Phoenix.Controller.redirect(redirect_opts)

{:stop, %Socket{redirected: {:live, _, %{to: to}}} = socket} ->
conn
Expand All @@ -60,6 +63,9 @@ defmodule Phoenix.LiveView.Controller do
end
end

defp put_status(conn, %{status: status}), do: Plug.Conn.put_status(conn, status)
defp put_status(conn, %{}), do: conn

defp ensure_format(conn) do
if Phoenix.Controller.get_format(conn) do
conn
Expand Down
10 changes: 10 additions & 0 deletions test/phoenix_live_view/integrations/params_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ defmodule Phoenix.LiveView.ParamsTest do
|> redirected_to() == "/"
end

test "hard redirects with a custom status", %{conn: conn} do
assert conn
|> put_serialized_session(
:on_handle_params,
&{:noreply, LiveView.redirect(&1, to: "/", status: 301)}
)
|> get("/counter/123?from=handle_params")
|> redirected_to(301) == "/"
end

test "hard redirect with flash message", %{conn: conn} do
conn =
put_serialized_session(conn, :on_handle_params, fn socket ->
Expand Down
8 changes: 8 additions & 0 deletions test/phoenix_live_view_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ defmodule Phoenix.LiveViewUnitTest do
assert redirect(@socket, to: "/foo").redirected == {:redirect, %{to: "/foo"}}
end

test "accepts a custom redirect status for local / external paths" do
assert redirect(@socket, to: "/foo", status: 301).redirected ==
{:redirect, %{to: "/foo", status: 301}}

assert redirect(@socket, external: "http://foo.com/bar", status: 301).redirected ==
{:redirect, %{external: "http://foo.com/bar", status: 301}}
end

test "allows external paths" do
assert redirect(@socket, external: "http://foo.com/bar").redirected ==
{:redirect, %{external: "http://foo.com/bar"}}
Expand Down

0 comments on commit f32b3d2

Please sign in to comment.