Skip to content

Commit

Permalink
fix: Fix link_attributes.
Browse files Browse the repository at this point in the history
  • Loading branch information
GSMLG-BOT committed Apr 15, 2022
1 parent 4277eaf commit 9d55077
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 171 deletions.
100 changes: 100 additions & 0 deletions lib/phoenix_webcomponent.ex
Expand Up @@ -69,4 +69,104 @@ defmodule Phoenix.WebComponent do
end
end

@doc """
Returns a list of attributes that make an element behave like a link.
For example, to make a button work like a link:
<button {link_attributes("/home")}>
Go back to home
</button>
However, this function is more often used to create buttons that
must invoke an action on the server, such as deleting an entity,
using the relevant HTTP protocol:
<button data-confirm="Are you sure?" {link_attributes("/product/1", method: :delete}>
Delete product
</button>
The `to` argument may be a string, a URI, or a tuple `{scheme, value}`.
See the examples below.
Note: using this function requires loading the JavaScript library
at `priv/static/phoenix_html.js`. See the `Phoenix.HTML` module
documentation for more information.
## Options
* `:method` - the HTTP method for the link. Defaults to `:get`.
* `:csrf_token` - a custom token to use when method is not `:get`.
This is used to ensure the request was sent by the user who
rendered the page. By default, CSRF tokens are generated through
`Plug.CSRFProtection`. You can set this option to `false`, to
disable token generation, or set it to your own token.
When the `:method` is set to `:get` and the `:to` URL contains query
parameters the generated form element will strip the parameters in
accordance with the [W3C](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4)
form specification.
## Data attributes
The following data attributes can also be manually set in the element:
* `data-confirm` - shows a confirmation prompt before generating and
submitting the form.
## Examples
iex> link_attributes("/world")
[data: [method: :get, to: "/world"]]
iex> link_attributes(URI.parse("https://elixir-lang.org"))
[data: [method: :get, to: "https://elixir-lang.org"]]
iex> link_attributes("/product/1", method: :delete)
[data: [csrf: Plug.CSRFProtection.get_csrf_token(), method: :delete, to: "/product/1"]]
If the URL is absolute, only certain schemas are allowed to
avoid JavaScript injection. For example, the following will fail:
iex> link_attributes("javascript:alert('hacked!')")
** (ArgumentError) unsupported scheme given as link. In case you want to link to an
unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
You can however explicitly render those unsafe schemes by using a tuple:
iex> link_attributes({:javascript, "alert('my alert!')"})
[data: [method: :get, to: ["javascript", 58, "alert('my alert!')"]]]
"""
def link_attributes(to, opts \\ []) do
to = valid_destination!(to)
method = Keyword.get(opts, :method, :get)
data = [method: method, to: to]

data =
if method == :get do
data
else
case Keyword.get(opts, :csrf_token, true) do
true -> [csrf: Phoenix.HTML.Tag.csrf_token_value(to)] ++ data
false -> data
csrf when is_binary(csrf) -> [csrf: csrf] ++ data
end
end

[data: data]
end

defp valid_destination!(%URI{} = uri) do
valid_destination!(URI.to_string(uri))
end

defp valid_destination!({:safe, to}) do
{:safe, valid_string_destination!(IO.iodata_to_binary(to))}
end

defp valid_destination!({other, to}) when is_atom(other) do
[Atom.to_string(other), ?:, to]
end

defp valid_destination!(to) do
valid_string_destination!(IO.iodata_to_binary(to))
end

@valid_uri_schemes ~w(http: https: ftp: ftps: mailto: news: irc: gopher:) ++
~w(nntp: feed: telnet: mms: rtsp: svn: tel: fax: xmpp:)

for scheme <- @valid_uri_schemes do
defp valid_string_destination!(unquote(scheme) <> _ = string), do: string
end

defp valid_string_destination!(to) do
if not match?("/" <> _, to) and String.contains?(to, ":") do
raise ArgumentError, """
unsupported scheme given as link. In case you want to link to an
unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}\
"""
else
to
end
end
end
6 changes: 3 additions & 3 deletions lib/phoenix_webcomponent/link.ex
Expand Up @@ -78,14 +78,14 @@ defmodule Phoenix.WebComponent.Link do

if method == :get do
# Call link attributes to validate `to`
[data: data] = Phoenix.HTML.link_attributes(to, [])
[data: data] = Phoenix.WebComponent.link_attributes(to, [])
content_tag(:a, text, [href: data[:to]] ++ Keyword.delete(opts, :csrf_token))
else
{csrf_token, opts} = Keyword.pop(opts, :csrf_token, true)
opts = Keyword.put_new(opts, :rel, "nofollow")

[data: data] =
Phoenix.HTML.link_attributes(to, method: method, csrf_token: csrf_token)
Phoenix.WebComponent.link_attributes(to, method: method, csrf_token: csrf_token)

content_tag(:a, text, [data: data, href: data[:to]] ++ opts)
end
Expand Down Expand Up @@ -139,7 +139,7 @@ defmodule Phoenix.WebComponent.Link do
|> Keyword.put_new(:method, :post)
|> Keyword.split([:method, :csrf_token])

link_attributes = Phoenix.HTML.link_attributes(to, link_opts)
link_attributes = Phoenix.WebComponent.link_attributes(to, link_opts)
content_tag(:"mwc-button", text, link_attributes ++ opts)
end

Expand Down
3 changes: 2 additions & 1 deletion test/phoenix_webcomponent/form_test.exs
@@ -1,7 +1,8 @@
defmodule Phoenix.WebComponent.FormTest do
use ExUnit.Case, async: true

import Phoenix.WebComponent
import Phoenix.HTML
import Phoenix.HTML.Form
import Phoenix.WebComponent.Form
doctest Phoenix.WebComponent.Form

Expand Down
84 changes: 42 additions & 42 deletions test/phoenix_webcomponent/link_test.exs
@@ -1,149 +1,149 @@
defmodule Phoenix.WebComponent.LinkTest do
use ExUnit.Case, async: true

import Phoenix.WebComponent
import Phoenix.HTML
import Phoenix.WebComponent.Link

test "link with post" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(link("hello", to: "/world", method: :post)) ==
assert safe_to_string(wc_link("hello", to: "/world", method: :post)) ==
~s[<a data-csrf="#{csrf_token}" data-method="post" data-to="/world" href="/world" rel="nofollow">hello</a>]
end

test "link with %URI{}" do
url = "https://elixir-lang.org/"

assert safe_to_string(link("elixir", to: url)) ==
safe_to_string(link("elixir", to: URI.parse(url)))
assert safe_to_string(wc_link("elixir", to: url)) ==
safe_to_string(wc_link("elixir", to: URI.parse(url)))

path = "/elixir"

assert safe_to_string(link("elixir", to: path)) ==
safe_to_string(link("elixir", to: URI.parse(path)))
assert safe_to_string(wc_link("elixir", to: path)) ==
safe_to_string(wc_link("elixir", to: URI.parse(path)))
end

test "link with put/delete" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(link("hello", to: "/world", method: :put)) ==
assert safe_to_string(wc_link("hello", to: "/world", method: :put)) ==
~s[<a data-csrf="#{csrf_token}" data-method="put" data-to="/world" href="/world" rel="nofollow">hello</a>]
end

test "link with put/delete without csrf_token" do
assert safe_to_string(link("hello", to: "/world", method: :put, csrf_token: false)) ==
assert safe_to_string(wc_link("hello", to: "/world", method: :put, csrf_token: false)) ==
~s[<a data-method="put" data-to="/world" href="/world" rel="nofollow">hello</a>]
end

test "link with :do contents" do
assert ~s[<a href="/hello"><p>world</p></a>] ==
safe_to_string(
link to: "/hello" do
wc_link to: "/hello" do
Phoenix.WebComponent.Tag.content_tag(:p, "world")
end
)

assert safe_to_string(
link(to: "/hello") do
wc_link(to: "/hello") do
"world"
end
) == ~s[<a href="/hello">world</a>]
end

test "link with scheme" do
assert safe_to_string(link("foo", to: "/javascript:alert(<1>)")) ==
assert safe_to_string(wc_link("foo", to: "/javascript:alert(<1>)")) ==
~s[<a href="/javascript:alert(&lt;1&gt;)">foo</a>]

assert safe_to_string(link("foo", to: {:safe, "/javascript:alert(<1>)"})) ==
assert safe_to_string(wc_link("foo", to: {:safe, "/javascript:alert(<1>)"})) ==
~s[<a href="/javascript:alert(<1>)">foo</a>]

assert safe_to_string(link("foo", to: {:javascript, "alert(<1>)"})) ==
assert safe_to_string(wc_link("foo", to: {:javascript, "alert(<1>)"})) ==
~s[<a href="javascript:alert(&lt;1&gt;)">foo</a>]

assert safe_to_string(link("foo", to: {:javascript, 'alert(<1>)'})) ==
assert safe_to_string(wc_link("foo", to: {:javascript, 'alert(<1>)'})) ==
~s[<a href="javascript:alert(&lt;1&gt;)">foo</a>]

assert safe_to_string(link("foo", to: {:javascript, {:safe, "alert(<1>)"}})) ==
assert safe_to_string(wc_link("foo", to: {:javascript, {:safe, "alert(<1>)"}})) ==
~s[<a href="javascript:alert(<1>)">foo</a>]

assert safe_to_string(link("foo", to: {:javascript, {:safe, 'alert(<1>)'}})) ==
assert safe_to_string(wc_link("foo", to: {:javascript, {:safe, 'alert(<1>)'}})) ==
~s[<a href="javascript:alert(<1>)">foo</a>]
end

test "link with invalid args" do
msg = "expected non-nil value for :to in link/2"
msg = "expected non-nil value for :to in wc_link/2"

assert_raise ArgumentError, msg, fn ->
link("foo", bar: "baz")
wc_link("foo", bar: "baz")
end

msg = "link/2 requires a keyword list as second argument"

assert_raise ArgumentError, msg, fn ->
link("foo", "/login")
wc_link("foo", "/login")
end

assert_raise ArgumentError, ~r"unsupported scheme given as link", fn ->
link("foo", to: "javascript:alert(1)")
wc_link("foo", to: "javascript:alert(1)")
end

assert_raise ArgumentError, ~r"unsupported scheme given as link", fn ->
link("foo", to: {:safe, "javascript:alert(1)"})
wc_link("foo", to: {:safe, "javascript:alert(1)"})
end

assert_raise ArgumentError, ~r"unsupported scheme given as link", fn ->
link("foo", to: {:safe, 'javascript:alert(1)'})
wc_link("foo", to: {:safe, 'javascript:alert(1)'})
end
end

test "button with post (default)" do
test "wc_button with post (default)" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(button("hello", to: "/world")) ==
~s[<button data-csrf="#{csrf_token}" data-method="post" data-to="/world">hello</button>]
assert safe_to_string(wc_button("hello", to: "/world")) ==
~s[<mwc-button data-csrf="#{csrf_token}" data-method="post" data-to="/world">hello</mwc-button>]
end

test "button with %URI{}" do
test "wc_button with %URI{}" do
url = "https://elixir-lang.org/"

assert safe_to_string(button("elixir", to: url, csrf_token: false)) ==
safe_to_string(button("elixir", to: URI.parse(url), csrf_token: false))
assert safe_to_string(wc_button("elixir", to: url, csrf_token: false)) ==
safe_to_string(wc_button("elixir", to: URI.parse(url), csrf_token: false))
end

test "button with post without csrf_token" do
assert safe_to_string(button("hello", to: "/world", csrf_token: false)) ==
~s[<button data-method="post" data-to="/world">hello</button>]
test "wc_button with post without csrf_token" do
assert safe_to_string(wc_button("hello", to: "/world", csrf_token: false)) ==
~s[<mwc-button data-method="post" data-to="/world">hello</mwc-button>]
end

test "button with get does not generate CSRF" do
assert safe_to_string(button("hello", to: "/world", method: :get)) ==
~s[<button data-method="get" data-to="/world">hello</button>]
test "wc_button with get does not generate CSRF" do
assert safe_to_string(wc_button("hello", to: "/world", method: :get)) ==
~s[<mwc-button data-method="get" data-to="/world">hello</mwc-button>]
end

test "button with do" do
test "wc_button with do" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

output =
safe_to_string(
button to: "/world", class: "small" do
wc_button to: "/world", class: "small" do
raw("<span>Hi</span>")
end
)

assert output ==
~s[<button class="small" data-csrf="#{csrf_token}" data-method="post" data-to="/world"><span>Hi</span></button>]
~s[<mwc-button class="small" data-csrf="#{csrf_token}" data-method="post" data-to="/world"><span>Hi</span></mwc-button>]
end

test "button with class overrides default" do
test "wc_button with class overrides default" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(button("hello", to: "/world", class: "btn rounded", id: "btn")) ==
~s[<button class="btn rounded" data-csrf="#{csrf_token}" data-method="post" data-to="/world" id="btn">hello</button>]
assert safe_to_string(wc_button("hello", to: "/world", class: "btn rounded", id: "btn")) ==
~s[<mwc-button class="btn rounded" data-csrf="#{csrf_token}" data-method="post" data-to="/world" id="btn">hello</mwc-button>]
end

test "button with invalid args" do
test "wc_button with invalid args" do
assert_raise ArgumentError, ~r/unsupported scheme given as link/, fn ->
button("foo", to: "javascript:alert(1)", method: :get)
wc_button("foo", to: "javascript:alert(1)", method: :get)
end
end
end

0 comments on commit 9d55077

Please sign in to comment.