The redirect rules of each site, answered on the request that asks for an old address.
MishkaCmsCore.Runtime.Redirect is the table and MishkaCmsCore.Runtime.States.RedirectsState
the ETS copy every request reads; this module is what stands between them and the rest of the
system — the form both addresses are stored in, the rule that a chain of redirects never comes
back on itself, and the answer a matching request gets.
Where a redirect is answered
MishkaCmsCore.Runtime.Plugs.SiteLookupPlug calls redirect/2 the moment the request's site is
known, before the session is touched and before any page is resolved, so a rule answers every
address the :browser pipeline reaches — a page that moved, and a file Plug.Static did not find,
which falls through to the router's catch-all.
A live navigation inside an open socket reaches no plug; lookup/3 answers it the same way, for
the LiveView that serves the site's pages to ask in handle_params/3.
The event
A request answered by a rule is heard on web:request:redirected, with the conn already carrying
its status and location and meta: %{site:, from:, to:, status:}. A plugin may change the conn,
the location included, or halt it to answer the request itself.
Summary
Functions
The rule site_id serves for request_path, as {location, status} — the target with the
request's query_string added when it has none of its own — or nil. Read from
MishkaCmsCore.Runtime.States.RedirectsState, with no query.
The validation every create and update of MishkaCmsCore.Runtime.Redirect runs: a rule that will
be served refuses a target that is its own source, and a target the site's other active rules lead
back from — to the source or to any address already on the way. A rule leaving the site, or for a
host MishkaCmsCore.Runtime.States.SiteState gives another site, ends the chain.
The change every create and update of MishkaCmsCore.Runtime.Redirect runs: from_path through
normalize_path/1 and to through normalize_target/1, each only when it is being written. A
value neither accepts is refused on its field, and so is a from_path that reserved?/1.
The address a rule answers, as it is stored and matched: one leading /, no scheme or host, no
query or fragment, no empty segment and so no trailing / but the root's, and every %XX escape
decoded. :error for what cannot be an address on a site: an empty value, whitespace or a control
character, a scheme other than http or https, or an escape that decodes to invalid UTF-8.
Where a rule may send a visitor, trimmed: a path on the site (/x, with a query or a fragment if
it has one) or an absolute http(s) URL with a host. :error for anything else — //host
included, which a browser reads as another host, and any \, which a browser reads as /, so
/\host is another host too.
Whether status tells a browser and a search engine the move is for good: 301 and 308.
Answers conn with the redirect site serves for its path, if any: the rule's status, the
location from lookup/3, and a halt. A conn no rule matches is returned untouched.
Whether path (normalized) is one of the CMS's own addresses, which no rule may take.
The name of status in the reader's language, such as 301 Moved permanently.
The statuses as {label, value} pairs for a select, each label in the reader's language.
The statuses a rule may answer with.
Whether user writes redirect rules from the dashboard: it holds
admin:dashboard:runtime:redirect, or a name above it. Which site's rules it writes is the
resource's policies' question.
Functions
@spec lookup(String.t(), String.t(), String.t()) :: {String.t(), pos_integer()} | nil
The rule site_id serves for request_path, as {location, status} — the target with the
request's query_string added when it has none of its own — or nil. Read from
MishkaCmsCore.Runtime.States.RedirectsState, with no query.
@spec no_loop(Ash.Changeset.t(), map()) :: :ok | {:error, keyword()}
The validation every create and update of MishkaCmsCore.Runtime.Redirect runs: a rule that will
be served refuses a target that is its own source, and a target the site's other active rules lead
back from — to the source or to any address already on the way. A rule leaving the site, or for a
host MishkaCmsCore.Runtime.States.SiteState gives another site, ends the chain.
Checked when the rule is written active, or restored, and when its addresses or its active
change — as the action runs, not while a form is being typed. The other rules are read with the
action's own actor, tenant and authorization, so a check the system runs reads as the system.
@spec normalize(Ash.Changeset.t(), map()) :: Ash.Changeset.t()
The change every create and update of MishkaCmsCore.Runtime.Redirect runs: from_path through
normalize_path/1 and to through normalize_target/1, each only when it is being written. A
value neither accepts is refused on its field, and so is a from_path that reserved?/1.
The address a rule answers, as it is stored and matched: one leading /, no scheme or host, no
query or fragment, no empty segment and so no trailing / but the root's, and every %XX escape
decoded. :error for what cannot be an address on a site: an empty value, whitespace or a control
character, a scheme other than http or https, or an escape that decodes to invalid UTF-8.
normalize_path("https://mishka.tools/images/blog/a.jpg?w=1") #=> {:ok, "/images/blog/a.jpg"}
normalize_path("old-page/") #=> {:ok, "/old-page"}
normalize_path("/fa/%D8%A8%D9%84%D8%A7%DA%AF") #=> {:ok, "/fa/بلاگ"}
normalize_path("/a b") #=> :error
Where a rule may send a visitor, trimmed: a path on the site (/x, with a query or a fragment if
it has one) or an absolute http(s) URL with a host. :error for anything else — //host
included, which a browser reads as another host, and any \, which a browser reads as /, so
/\host is another host too.
@spec permanent?(pos_integer()) :: boolean()
Whether status tells a browser and a search engine the move is for good: 301 and 308.
@spec redirect(Plug.Conn.t(), %{id: String.t()}) :: Plug.Conn.t()
Answers conn with the redirect site serves for its path, if any: the rule's status, the
location from lookup/3, and a halt. A conn no rule matches is returned untouched.
A plugin on web:request:redirected hears every one first — see the moduledoc.
Whether path (normalized) is one of the CMS's own addresses, which no rule may take.
@spec status_label(pos_integer()) :: String.t()
The name of status in the reader's language, such as 301 Moved permanently.
@spec status_options() :: [{String.t(), pos_integer()}]
The statuses as {label, value} pairs for a select, each label in the reader's language.
@spec statuses() :: [301 | 302 | 307 | 308]
The statuses a rule may answer with.
Whether user writes redirect rules from the dashboard: it holds
admin:dashboard:runtime:redirect, or a name above it. Which site's rules it writes is the
resource's policies' question.