2020-05-21 02:10:50 +00:00
|
|
|
defmodule Clacks.Webmention.Endpoint do
|
|
|
|
require Logger
|
|
|
|
|
|
|
|
@spec find_endpoint(url :: String.t()) :: URI.t() | nil
|
|
|
|
def find_endpoint(url) do
|
|
|
|
case find_endpoint_by_header(url) do
|
|
|
|
nil ->
|
|
|
|
find_endpoint_by_html(url)
|
|
|
|
|
|
|
|
:error ->
|
|
|
|
:error
|
|
|
|
|
|
|
|
endpoint ->
|
|
|
|
endpoint
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp find_endpoint_by_header(url) do
|
|
|
|
case Clacks.HTTP.head(url) do
|
|
|
|
{:ok, %HTTPoison.Response{headers: headers, request: %HTTPoison.Request{url: final_url}}} ->
|
|
|
|
headers
|
|
|
|
|> Enum.filter(fn {name, _} -> String.downcase(name) == "link" end)
|
|
|
|
|> webmention_link()
|
|
|
|
|> case do
|
|
|
|
nil ->
|
|
|
|
nil
|
|
|
|
|
|
|
|
str when is_binary(str) ->
|
|
|
|
URI.merge(final_url, str)
|
|
|
|
end
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
Logger.warn("Unable to find Webmention endpoint for '#{url}': #{reason}")
|
|
|
|
:error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp webmention_link([]), do: nil
|
|
|
|
|
|
|
|
defp webmention_link([{_, value} | rest]) do
|
|
|
|
String.split(value, ",")
|
|
|
|
|> Enum.map(&parse_link_header/1)
|
|
|
|
|> Enum.find(fn {rels, _} -> "webmention" in rels end)
|
|
|
|
|> case do
|
|
|
|
nil ->
|
|
|
|
webmention_link(rest)
|
|
|
|
|
|
|
|
{_, res} ->
|
|
|
|
res
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp parse_link_header(value) do
|
|
|
|
[value | params] = String.split(value, ";")
|
|
|
|
|
|
|
|
uri_reference =
|
|
|
|
value
|
|
|
|
|> String.trim()
|
2020-05-25 22:18:21 +00:00
|
|
|
|> String.slice(1..-2)
|
2020-05-21 02:10:50 +00:00
|
|
|
|
|
|
|
{_, rel} =
|
|
|
|
params
|
|
|
|
|> Enum.map(fn str ->
|
|
|
|
str = String.trim(str)
|
|
|
|
[name | rest] = String.split(str, "=")
|
|
|
|
rest = Enum.join(rest, "=")
|
|
|
|
|
|
|
|
value =
|
|
|
|
if String.starts_with?(rest, "\"") do
|
|
|
|
{_, rest} = String.split_at(rest, 1)
|
|
|
|
|
|
|
|
if String.ends_with?(rest, "\"") do
|
|
|
|
{rest, _} = String.split_at(rest, -1)
|
|
|
|
rest
|
|
|
|
else
|
|
|
|
rest
|
|
|
|
end
|
|
|
|
else
|
|
|
|
rest
|
|
|
|
end
|
|
|
|
|
|
|
|
{name, value}
|
|
|
|
end)
|
|
|
|
|> Enum.find(fn {name, _} -> String.downcase(name) == "rel" end)
|
|
|
|
|
|
|
|
rels = String.split(rel, ~r/\s+/) |> Enum.map(&String.downcase/1)
|
|
|
|
|
|
|
|
{rels, uri_reference}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp find_endpoint_by_html(url) do
|
|
|
|
case Clacks.HTTP.get(url) do
|
|
|
|
{:ok, %HTTPoison.Response{body: body, request: %HTTPoison.Request{url: final_url}}} ->
|
|
|
|
{:ok, doc} = Floki.parse_document(body)
|
|
|
|
|
|
|
|
Floki.find(doc, "link[rel~=webmention], a[rel~=webmention]")
|
|
|
|
|> Enum.reduce_while(nil, fn el, _acc ->
|
|
|
|
case Floki.attribute(el, "href") do
|
|
|
|
[href] when is_binary(href) ->
|
|
|
|
{:halt, href}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
{:cont, nil}
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|> case do
|
|
|
|
nil ->
|
|
|
|
nil
|
|
|
|
|
|
|
|
str when is_binary(str) ->
|
|
|
|
URI.merge(final_url, str)
|
|
|
|
end
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
Logger.warn("Unable to find Webmention endpoint for '#{url}': #{reason}")
|
|
|
|
:error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|