Limen.Challenge.Page behaviour (limen v0.2.1)

Copy Markdown View Source

The challenge interstitial, and how to make it your own.

The page has no inline scripts or styles: it loads solver.js and challenge.css from the challenge path, reads the challenge from data- attributes, and is served with its own strict Content-Security-Policy (content_security_policy/0).

Your own words

The page is in English unless the instance's :page (an option of the :challenge configuration) is a module of yours implementing this behaviour. text/2 is called for every page served, with the request, so it can speak the visitor's language:

defmodule MyAppWeb.ChallengePage do
  use Limen.Challenge.Page
  use Gettext, backend: MyAppWeb.Gettext

  @impl true
  def text(conn, vars) do
    Gettext.put_locale(MyAppWeb.Gettext, MyAppWeb.Locale.from_conn(conn))

    %{
      lang: Gettext.get_locale(MyAppWeb.Gettext),
      title: gettext("Checking your browser"),
      heading: gettext("Checking your browser"),
      message: gettext("This takes a moment and only happens once in a while."),
      no_js: no_js(vars)
    }
  end

  defp no_js(%{seconds: nil}), do: gettext("Please enable JavaScript to continue.")

  defp no_js(%{seconds: seconds}),
    do: gettext("You will be taken to the page in %{seconds} seconds.", seconds: seconds)
end

config :my_app, Limen, challenge: [page: MyAppWeb.ChallengePage]

It returns any of these keys, as plain text; the rest keep their English defaults (see default_text/1):

  • :lang - the page's language, for <html lang>;
  • :title, :heading and :message - what the visitor reads while the check runs;
  • :no_js - what visitors without JavaScript read: how long they wait when vars.seconds is an integer (no_js: {:meta_refresh, seconds}), or that they need JavaScript when it is nil (no_js: :deny);
  • :solved, :failed and :unsupported - what the script shows when the check succeeds, cannot run, or the browser has no Web Workers.

text/2 runs on the request path of every challenged navigation: no calls to processes, and nothing slow.

Your own page

render/1 returns the whole page. Override it to change the markup, for example to add your own stylesheet, which the page's Content-Security-Policy allows as long as it comes from your own origin:

@impl true
def render(assigns) do
  Limen.Challenge.Page.template(%{assigns | stylesheets: assigns.stylesheets ++ ["/assets/challenge.css"]})
end

A page of your own must keep the contract with solver.js, and use no inline scripts or styles (the Content-Security-Policy blocks them): an element with the id limen-challenge and data-token, data-difficulty and data-worker attributes (and optionally data-text-solved, data-text-failed and data-text-unsupported), an element with the id limen-status for messages, a form with the id limen-form posting to verify the hidden fields token, nonce (empty), return_to and instance, and a <script src> for solver. template/1 is the reference. Assigns are not escaped: escape them, as template/1 does.

Assigns:

  • :text - the texts above, merged over the defaults;
  • :token, :difficulty, :return_to and :instance - the challenge;
  • :solver, :worker and :verify - the URLs of the script, its worker and the verification endpoint;
  • :stylesheets - stylesheet URLs, Limen's by default;
  • :refresh - the content of the no-JavaScript meta refresh, or nil.

Summary

Types

What text/2 knows about the page: the no-JavaScript wait, if any.

Callbacks

The page, from the assigns described in the module documentation.

The page's texts for the request, in any subset: see the module documentation for the keys.

Functions

The Content-Security-Policy the page is served with.

The English texts, for vars.

Renders Limen's page from assigns, escaping them.

Types

vars()

@type vars() :: %{seconds: non_neg_integer() | nil}

What text/2 knows about the page: the no-JavaScript wait, if any.

Callbacks

render(assigns)

@callback render(assigns :: map()) :: iodata()

The page, from the assigns described in the module documentation.

text(conn, vars)

@callback text(conn :: Plug.Conn.t(), vars()) :: map() | keyword()

The page's texts for the request, in any subset: see the module documentation for the keys.

Functions

content_security_policy()

@spec content_security_policy() :: String.t()

The Content-Security-Policy the page is served with.

default_text(map)

@spec default_text(vars()) :: map()

The English texts, for vars.

template(assigns)

@spec template(map()) :: String.t()

Renders Limen's page from assigns, escaping them.