defmodule FastUaparser do @moduledoc """ Documentation for `FastUaparser`. """ alias FastUaparser.{UserAgent, EngineInfo, VersionInfo, OperatingSystem, DeviceInfo} require Logger @user_agent_products_regex Regex.compile!("([^/ ]+)/([^ ]+) *(\\([^)]+\\))*") @ignore_products_as_engine ["Mozilla", "Gecko", "AppleWebKit"] @doc """ Parses user agent. """ @spec parse_ua(binary) :: {:ok, UserAgent.t()} | {:error, any()} def parse_ua(user_agent) when is_binary(user_agent) do with splitted <- Regex.scan(@user_agent_products_regex, user_agent), {:ok, engine_info} <- extract_engine_info(splitted), {:ok, device_info} <- extract_device_info(splitted) do {:ok, %UserAgent{engine_info: engine_info, device_info: device_info}} else err -> Logger.error(inspect(err)) {:error, :bad_format} end end @doc """ Extract client engine info from a splitted regex list like: ```elixir [["Firefox/5.0 (Linux)", "Firefox", "5.0", "Linux"] | _] ``` """ @spec extract_engine_info(list()) :: {:ok, EngineInfo.t()} | {:error, :bad_format | :empty} def extract_engine_info(splitted) when is_list(splitted) do splitted |> Enum.reduce([], fn [_full_re, name, version | _extension], acc -> [{name, version} | acc] _bad_format, acc -> acc end) |> Enum.reverse() |> do_extract_engine_info() end defp extract_device_info([[_full, _product_name, _version, os_string] | _t]) do result = os_string |> String.replace(~r/[()]/, "") |> String.split(";") |> Enum.map(&String.trim/1) case result do ["Linux", "U", android_version, hardware_info] -> # Detected Android with ["Android", version] <- String.split(android_version) do parts = do_extract_hardware_info_parts(hardware_info) {:ok, %DeviceInfo{ manufaturer: nil, model: List.first(parts), version: List.last(parts), os: %OperatingSystem{ family: "Android", version: parse_version(version) } }} end [device_type, details] when device_type in ["iPad", "iPod", "iPhone"] -> # Detected iOS with ["CPU", device_model, "OS", os_version | _] <- String.split(details) do {:ok, %DeviceInfo{ manufaturer: "Apple Inc.", model: device_model, os: %OperatingSystem{ family: device_type, version: parse_version(String.replace(os_version, "_", ".")) } }} end ["X11", info] -> {:ok, %DeviceInfo{ os: %OperatingSystem{ family: "Linux", version: "X11 #{info}" } }} _ -> # invalid input {:error, :invalid_input} end end defp extract_device_info([_ | tail]), do: extract_device_info(tail) defp extract_device_info([]), do: {:error, :not_found} ### # Helpers ### Enum.each(@ignore_products_as_engine, fn name -> # To ignore strings like Mozilla and AppleWebKit become the # detected client engine. defp do_extract_engine_info([{unquote(name), _} | tail]), do: do_extract_engine_info(tail) end) defp do_extract_engine_info([{name, version} | _]) do {:ok, %EngineInfo{name: name, version: parse_version(version)}} end defp do_extract_engine_info([]), do: {:error, :empty} defp do_extract_engine_info(_), do: {:error, :bad_format} defp parse_version(version) do case String.split(version, ".") do [v] -> %VersionInfo{code: v} [major, minor] -> %VersionInfo{major: major, minor: minor, code: version} [major, minor, patch] -> %VersionInfo{major: major, minor: minor, patch: patch, code: version} [major, minor, patch, patch_minor | _] -> %VersionInfo{ major: major, minor: minor, patch: patch, patch_minor: patch_minor, code: version } end end defp do_extract_hardware_info_parts(hardware_info) do hardware_info |> String.reverse() |> String.split(" ", parts: 2) |> Enum.map(&String.reverse/1) |> Enum.reverse() end end