You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

323 lines
8.7 KiB

  1. defmodule PlugMicropub do
  2. @moduledoc """
  3. A Plug for building a Micropub server.
  4. To use:
  5. """
  6. use Plug.Router
  7. plug(:match)
  8. plug(:dispatch)
  9. # Plug Callbacks
  10. @doc false
  11. def init(opts) do
  12. handler =
  13. Keyword.get(opts, :handler) || raise ArgumentError, "Micropub Plug requires :handler option"
  14. json_encoder =
  15. Keyword.get(opts, :json_encoder) ||
  16. raise ArgumentError, "Micropub Plug requires :json_encoder option"
  17. [handler: handler, json_encoder: json_encoder]
  18. end
  19. @doc false
  20. def call(conn, opts) do
  21. conn = put_private(conn, :plug_micropub, opts)
  22. super(conn, opts)
  23. end
  24. # Routes
  25. post "/" do
  26. with {:ok, access_token, conn} <- get_access_token(conn),
  27. {:ok, action, conn} <- get_action(conn) do
  28. handle_action(action, access_token, conn)
  29. else
  30. error -> send_error(conn, error)
  31. end
  32. end
  33. get "/" do
  34. with {:ok, access_token, conn} <- get_access_token(conn),
  35. {:ok, query} <- get_query(conn) do
  36. handle_query(query, access_token, conn)
  37. else
  38. error -> send_error(conn, error)
  39. end
  40. end
  41. post "/media" do
  42. handler = conn.private[:plug_micropub][:handler]
  43. with {:ok, access_token, conn} <- get_access_token(conn),
  44. {:ok, file} <- get_file(conn),
  45. {:ok, url} <- handler.handle_media(file, access_token) do
  46. conn
  47. |> put_resp_header("location", url)
  48. |> send_resp(:created, "")
  49. else
  50. error -> send_error(conn, error)
  51. end
  52. end
  53. # Internal Functions
  54. defp send_content(conn, content) do
  55. json_encoder = conn.private[:plug_micropub][:json_encoder]
  56. body = json_encoder.encode!(content)
  57. conn
  58. |> put_resp_content_type("application/json")
  59. |> send_resp(:ok, body)
  60. end
  61. defp send_error(conn, {:error, error}) do
  62. body = %{error: error}
  63. _send_error(conn, body)
  64. end
  65. defp send_error(conn, {:error, error, description}) do
  66. body = %{error: error, error_description: description}
  67. _send_error(conn, body)
  68. end
  69. defp _send_error(conn, body) do
  70. json_encoder = conn.private[:plug_micropub][:json_encoder]
  71. code = get_error_code(body.error)
  72. body = json_encoder.encode!(body)
  73. conn
  74. |> put_resp_content_type("application/json")
  75. |> send_resp(code, body)
  76. end
  77. defp get_error_code(:insufficient_scope), do: :unauthorized
  78. defp get_error_code(:invalid_request), do: :bad_request
  79. defp get_error_code(code), do: code
  80. defp get_action(conn) do
  81. {action, body_params} = Map.pop(conn.body_params, "action")
  82. conn = %Plug.Conn{conn | body_params: body_params}
  83. case action do
  84. nil ->
  85. {:ok, :create, conn}
  86. action when action in ["delete", "undelete", "update"] ->
  87. {:ok, String.to_existing_atom(action), conn}
  88. _ ->
  89. {:error, :invalid_request}
  90. end
  91. end
  92. defp get_query(conn) do
  93. case Map.fetch(conn.query_params, "q") do
  94. {:ok, query} when query in ["config", "source", "syndicate-to"] ->
  95. {:ok, String.to_existing_atom(query)}
  96. _ ->
  97. {:error, :invalid_request}
  98. end
  99. end
  100. defp get_file(conn) do
  101. case Map.fetch(conn.body_params, "file") do
  102. {:ok, file} -> {:ok, file}
  103. :error -> {:error, :invalid_request}
  104. end
  105. end
  106. defp get_access_token(conn) do
  107. {access_token, body_params} = Map.pop(conn.body_params, "access_token")
  108. conn = %Plug.Conn{conn | body_params: body_params}
  109. case access_token do
  110. nil -> parse_auth_header(conn)
  111. access_token -> {:ok, access_token, conn}
  112. end
  113. end
  114. defp parse_auth_header(conn) do
  115. with [header] <- get_req_header(conn, "authorization"),
  116. _ = IO.inspect(header),
  117. "Bearer" <> token <- header,
  118. do: {:ok, String.trim(token), conn},
  119. else: (_ -> {:error, :unauthorized})
  120. end
  121. defp handle_action(:create, access_token, conn) do
  122. content_type = conn |> get_req_header("content-type") |> List.first()
  123. handler = conn.private[:plug_micropub][:handler]
  124. with {:ok, type, properties} <- parse_create_body(content_type, conn.body_params),
  125. {:ok, code, url} <- handler.handle_create(type, properties, access_token) do
  126. conn
  127. |> put_resp_header("location", url)
  128. |> send_resp(code, "")
  129. else
  130. error -> send_error(conn, error)
  131. end
  132. end
  133. defp handle_action(:update, access_token, conn) do
  134. content_type = conn |> get_req_header("content-type") |> List.first()
  135. with "application/json" <- content_type,
  136. {url, properties} when is_binary(url) <- Map.pop(conn.body_params, "url"),
  137. {:ok, replace, add, delete} <- parse_update_properties(properties),
  138. do: do_update(conn, access_token, url, replace, add, delete),
  139. else: (_ -> send_error(conn, {:error, :invalid_request}))
  140. end
  141. defp handle_action(:delete, access_token, conn) do
  142. with {:ok, url} <- Map.fetch(conn.body_params, "url"),
  143. do: do_delete(conn, access_token, url),
  144. else: (_ -> send_error(conn, {:error, :invalid_request}))
  145. end
  146. defp handle_action(:undelete, access_token, conn) do
  147. with {:ok, url} <- Map.fetch(conn.body_params, "url"),
  148. do: do_undelete(conn, access_token, url),
  149. else: (_ -> send_error(conn, {:error, :invalid_request}))
  150. end
  151. defp handle_query(:config, access_token, conn) do
  152. handler = conn.private[:plug_micropub][:handler]
  153. case handler.handle_config_query(access_token) do
  154. {:ok, content} -> send_content(conn, content)
  155. error -> send_error(conn, error)
  156. end
  157. end
  158. defp handle_query(:source, access_token, conn) do
  159. with {:ok, url} <- Map.fetch(conn.query_params, "url"),
  160. do: do_source_query(conn, access_token, url),
  161. else: (_ -> send_error(conn, {:error, :invalid_request}))
  162. end
  163. defp handle_query(:"syndicate-to", access_token, conn) do
  164. handler = conn.private[:plug_micropub][:handler]
  165. case handler.handle_syndicate_to_query(access_token) do
  166. {:ok, content} -> send_content(conn, content)
  167. error -> send_error(conn, error)
  168. end
  169. end
  170. defp parse_update_properties(properties) do
  171. properties = Map.take(properties, ["replace", "add", "delete"])
  172. valid? =
  173. Enum.all?(properties, fn
  174. {"delete", prop} when is_list(prop) ->
  175. Enum.all?(prop, &is_binary/1)
  176. {_k, prop} when is_map(prop) ->
  177. Enum.all?(prop, fn
  178. {_k, v} when is_list(v) -> true
  179. _ -> false
  180. end)
  181. _ ->
  182. false
  183. end)
  184. if valid? do
  185. replace = Map.get(properties, "replace", %{})
  186. add = Map.get(properties, "add", %{})
  187. delete = Map.get(properties, "delete", %{})
  188. {:ok, replace, add, delete}
  189. else
  190. :error
  191. end
  192. end
  193. defp do_update(conn, access_token, url, replace, add, delete) do
  194. handler = conn.private[:plug_micropub][:handler]
  195. case handler.handle_update(url, replace, add, delete, access_token) do
  196. :ok ->
  197. send_resp(conn, :no_content, "")
  198. {:ok, url} ->
  199. conn
  200. |> put_resp_header("location", url)
  201. |> send_resp(:created, "")
  202. error ->
  203. send_error(conn, error)
  204. end
  205. end
  206. defp do_delete(conn, access_token, url) do
  207. handler = conn.private[:plug_micropub][:handler]
  208. case handler.handle_delete(url, access_token) do
  209. :ok -> send_resp(conn, :no_content, "")
  210. error -> send_error(conn, error)
  211. end
  212. end
  213. defp do_undelete(conn, access_token, url) do
  214. handler = conn.private[:plug_micropub][:handler]
  215. case handler.handle_undelete(url, access_token) do
  216. :ok ->
  217. send_resp(conn, :no_content, "")
  218. {:ok, url} ->
  219. conn
  220. |> put_resp_header("location", url)
  221. |> send_resp(:created, "")
  222. error ->
  223. send_error(conn, error)
  224. end
  225. end
  226. defp do_source_query(conn, access_token, url) do
  227. handler = conn.private[:plug_micropub][:handler]
  228. properties = Map.get(conn.query_params, "properties", [])
  229. case handler.handle_source_query(url, properties, access_token) do
  230. {:ok, content} -> send_content(conn, content)
  231. error -> send_error(conn, error)
  232. end
  233. end
  234. defp parse_create_body("application/json", params) do
  235. with {:ok, ["h-" <> type]} <- Map.fetch(params, "type"),
  236. {:ok, properties} when is_map(properties) <- Map.fetch(params, "properties") do
  237. properties =
  238. properties
  239. |> Enum.reject(&match?({"mp-" <> _, _}, &1))
  240. |> Map.new()
  241. {:ok, type, properties}
  242. else
  243. _ -> {:error, :invalid_request}
  244. end
  245. end
  246. defp parse_create_body(_, params) do
  247. with {type, params} when is_binary(type) <- Map.pop(params, "h") do
  248. properties =
  249. params
  250. |> Enum.reject(&match?({"mp-" <> _, _}, &1))
  251. |> Enum.map(fn {k, v} -> {k, List.wrap(v)} end)
  252. |> Map.new()
  253. {:ok, type, properties}
  254. else
  255. _ -> {:error, :invalid_request}
  256. end
  257. end
  258. end