defmodule Phoenix.WebComponent do
@moduledoc """
The default building blocks for working with HTML safely
in Phoenix.
This library provides three main functionalities:
* Form handling (with CSRF protection)
* A tiny JavaScript library to enhance applications
## Form handling
See `Phoenix.WebComponent.Form`.
## JavaScript library
This project ships with a tiny bit of JavaScript that listens
to all click events to:
* Support `data-confirm="message"` attributes, which shows
a confirmation modal with the given message
* Support `data-method="patch|post|put|delete"` attributes,
which sends the current click as a PATCH/POST/PUT/DELETE
HTTP request. You will need to add `data-to` with the URL
and `data-csrf` with the CSRF token value. See
`link_attributes/2` for a function that wraps it all up
for you
* Dispatch a "phoenix.link.click" event. You can listen to this
event to customize the behaviour above. Returning false from
this event will disable `data-method`. Stopping propagation
will disable `data-confirm`
To use the functionality above, you must load `priv/static/phoenix_html.js`
into your build tool.
### Overriding the default confirm behaviour
You can override the default confirmation behaviour by hooking
into `phoenix.link.click`. Here is an example:
```javascript
// listen on document.body, so it's executed before the default of
// phoenix_html, which is listening on the window object
document.body.addEventListener('phoenix.link.click', function (e) {
// Prevent default implementation
e.stopPropagation();
// Introduce alternative implementation
var message = e.target.getAttribute("data-confirm");
if(!message){ return true; }
vex.dialog.confirm({
message: message,
callback: function (value) {
if (value == false) { e.preventDefault(); }
}
})
}, false);
```
"""
@doc false
defmacro __using__(_) do
quote do
import Phoenix.WebComponent.FormHelper
import Phoenix.WebComponent.Link
import Phoenix.WebComponent.Markdown
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