Packages
indieweb
0.0.64
0.0.66
0.0.65
0.0.64
0.0.63
0.0.62
0.0.61
0.0.60
0.0.59
0.0.58
0.0.56
0.0.55
0.0.54
0.0.53
0.0.51
0.0.50
0.0.49
0.0.48
0.0.47
0.0.46
0.0.42
0.0.41
0.0.40
0.0.39
0.0.38
0.0.37
0.0.36
0.0.35
0.0.34
0.0.33
0.0.32
0.0.31
0.0.30
0.0.29
0.0.28
0.0.27
0.0.26
0.0.25
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.3
0.0.2
0.0.1
Collection of common IndieWeb utilites like authorship resolution, Webmention, post type discovery and IndieAuth.
Current section
Files
Jump to
Current section
Files
lib/indieweb/hcard.ex
defmodule IndieWeb.HCard do
@moduledoc """
Extracts the representative [h-card](http://indieweb.org/h-card) of the provided URI.
"""
require Logger
alias IndieWeb.{Http, MF2}
@doc """
Obtains a remote page and determines the h-card of the MF2.
This takes a few approaches to find a h-card for the page in question.
It'll first search for a top-level h-card on the page.
If there isn't an obvious one, it'll search for a representative h-card.
"""
@spec resolve(binary()) :: {:ok, map()} | {:error, any()}
def resolve(uri) when is_binary(uri) do
approaches = [
extract_from_canonical_item: fn url ->
with(
{:ok, mf2} <- MF2.Remote.fetch(url),
{:ok, item_mf2} <- MF2.get_item_for_url(mf2, url)
) do
fetch_representative(item_mf2, url)
else
{:error, _error} -> nil
end
end,
fetch_representative: &__MODULE__.fetch_representative/1
]
result =
Enum.find_value(approaches, fn {approach, method} ->
Logger.info("Attempting to resolve h-card.",
approach: approach,
url: uri
)
case method.(uri) do
{:ok, _} = resp -> resp
_ -> false
end
end)
case result do
{:ok, hcard} -> do_cleanup_hcard(hcard)
_ -> {:error, :no_hcard_found}
end
end
@doc """
Obtains the representative h-card of the provided URI.
The [steps for parsing a representative h-card][1] are as follows:
* If the page contains an `h-card` with `uid` and `url` properties
both matching the page URL, the first such `h-card` is the
**representative h-card**.
* If no representative h-card was found, if the page contains an
`h-card` with a url property value which also has a rel=me
relation, the first such `h-card` is the **representative h-card**
* If no representative `h-card` was found, if the page contains one
single `h-card`, and the `h-card` has a url property matching the
page URL, that `h-card` is the representative `h-card`
* <thunk>
[1]: http://microformats.org/wiki/representative-h-card-parsing
"""
def fetch_representative(uri) when is_binary(uri) do
Logger.debug("Fetching page MF2.", uri: uri)
case MF2.Remote.fetch(uri) do
{:ok, mf2} -> fetch_representative(mf2, uri)
{:error, _} = error -> error
end
end
@doc """
Obtains the representative h-card of the provided URI from the provided MF2.
See fetch_representative/1 for more information.
"""
def fetch_representative(mf2, uri) do
%{scheme: scheme, authority: authority} = URI.parse(uri)
hostname = "#{scheme}://#{authority}"
Logger.debug("Extracted hostname for this uri.", hostname: hostname, uri: uri)
approaches = [
{:uid_url_of_page, &do_find_uid_url/2},
{:matching_rel_me_to_hcard, &do_find_with_matching_relme/2},
{:resolve_one_hcard, &do_find_solo/2}
]
Enum.find_value(approaches, {:error, reason: :no_representative_hcard_found}, fn {identifier,
approach} ->
Logger.debug("Attempting to find h-card.", approach: identifier, url: uri)
case approach.(mf2, {hostname, uri}) do
nil ->
Logger.debug("Couldn't find h-card with this approach.", approach: identifier, url: uri)
false
hcard ->
Logger.info("Found h-card.", approach: identifier, url: uri)
do_cleanup_hcard(hcard)
end
end)
end
# `rel=author` and `rel=home` are both used here in hopes of expanding those
# for whose have might MicroFormats2 support but not fully. This is not spec-compliant
# but I know those values are used and thus expanding to it only increases adoption.
defp do_find_with_matching_relme(mf2, {_, url}) do
author_rel_urls = extract_author_identity_urls(mf2, url)
cards = MF2.extract_all(mf2, "card")
Logger.debug("Found hCards and rel-me links.",
rel_mes: Enum.count(author_rel_urls),
hcards: Enum.count(cards)
)
if Enum.empty?(author_rel_urls) or Enum.empty?(cards) do
nil
else
cards
|> Enum.map(fn card ->
card
|> MF2.get_value!("url", [])
|> Enum.map(&{card, &1})
end)
|> List.flatten()
|> Enum.find_value(nil, fn {card, card_url} ->
card_uri = Http.make_absolute_uri(card_url, url)
if Enum.member?(author_rel_urls, card_uri) do
card
else
nil
end
end)
end
end
defp do_find_uid_url(mf2, {host, uri}) do
mf2
|> MF2.extract_all("card")
|> Enum.find(nil, fn hcard ->
hcard_url_matches? =
hcard
|> MF2.get_value!("url", ["/"])
|> Enum.map(&Http.make_absolute_uri(&1, host))
|> Enum.member?(uri)
hcard_uid_matches? =
hcard
|> MF2.get_value!("uid", [nil])
|> Enum.map(&Http.make_absolute_uri(&1, host))
|> Enum.member?(uri)
hcard_url_matches? && hcard_uid_matches?
end)
end
defp do_find_solo(mf2, _) do
cards = MF2.extract_all(mf2, "card")
if Enum.count(cards) == 1 do
List.first(cards)
end
end
defp do_cleanup_hcard(hcard) do
properties = hcard["properties"]
values = properties |> Map.values() |> List.flatten() |> Enum.uniq() |> Enum.reject(&(&1 == ""))
if values == hcard["properties"]["url"] do
Enum.find_value(hcard["properties"]["url"], fn url ->
fetch_representative(url)
end)
else
{:ok, hcard}
end
end
defp extract_author_identity_urls(%{"rels" => rels}, url) do
~w(home me author)
|> Enum.map(fn key -> Map.get(rels, key, []) end)
|> List.flatten()
|> Enum.map(&Http.make_absolute_uri(&1, url))
|> Enum.map(&URI.parse/1)
|> Enum.map(&URI.to_string/1)
|> List.wrap()
end
defp extract_author_identity_urls(_, _) do
[]
end
end
# SPDX-License-Identifier: AGPL-3.0-only