diff options
Diffstat (limited to 'lib')
38 files changed, 1047 insertions, 298 deletions
diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex new file mode 100644 index 000000000..14d47aaba --- /dev/null +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -0,0 +1,243 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Frontend do + use Mix.Task + + import Mix.Pleroma + + @shortdoc "Manages bundled Pleroma frontends" + @moduledoc File.read!("docs/administration/CLI_tasks/frontend.md") + + @frontends %{ + "admin" => %{"project" => "pleroma/admin-fe"}, + "kenoma" => %{"project" => "lambadalambda/kenoma"}, + "mastodon" => %{"project" => "pleroma/mastofe"}, + "pleroma" => %{"project" => "pleroma/pleroma-fe"}, + "fedi" => %{"project" => "dockyard/fedi-fe"} + } + @known_frontends Map.keys(@frontends) + + @ref_local "__local__" + @ref_develop "__develop__" + @ref_stable "__stable__" + + @pleroma_gitlab_host "git.pleroma.social" + + def run(["install", "none" | _args]) do + shell_info("Skipping frontend installation because none was requested") + "none" + end + + def run(["install", unknown_fe | _args]) when unknown_fe not in @known_frontends do + shell_error( + "Frontend \"#{unknown_fe}\" is not known. Known frontends are: #{ + Enum.join(@known_frontends, ", ") + }" + ) + end + + def run(["install", frontend | args]) do + log_level = Logger.level() + Logger.configure(level: :warn) + {:ok, _} = Application.ensure_all_started(:pleroma) + + {options, [], []} = + OptionParser.parse( + args, + strict: [ + ref: :string, + path: :string, + develop: :boolean, + static_dir: :string + ] + ) + + instance_static_dir = + with nil <- options[:static_dir] do + Pleroma.Config.get!([:instance, :static_dir]) + end + + ref = + case options[:path] do + nil -> + ref0 = + cond do + options[:ref] -> options[:ref] + options[:develop] -> @ref_develop + true -> @ref_stable + end + + web_frontend_ref(frontend, ref0) + + path -> + local_path_frontend_ref(path) + end + + dest = + Path.join([ + instance_static_dir, + "frontends", + frontend, + ref + ]) + + fe_label = "#{frontend} (#{ref})" + + from = + with nil <- options[:path] do + Pleroma.Utils.command_required!("yarn") + + url = archive_url(frontend, ref) + + tmp_dir = Path.join(dest, "tmp/src") + + shell_info("Downloading #{fe_label} to #{tmp_dir}") + :ok = download_frontend(url, tmp_dir) + + shell_info("Building #{fe_label} (this will take some time)") + :ok = build_frontend(frontend, tmp_dir) + tmp_dir + end + + shell_info("Installing #{fe_label} to #{dest}") + + :ok = install_frontend(frontend, from, dest) + + shell_info("Frontend #{fe_label} installed to #{dest}") + Logger.configure(level: log_level) + ref + end + + defp download_frontend(url, dest) do + with {:ok, %{status: 200, body: zip_body}} <- + Pleroma.HTTP.get(url, [], timeout: 120_000, recv_timeout: 120_000), + {:ok, unzipped} <- :zip.unzip(zip_body, [:memory]) do + File.rm_rf!(dest) + File.mkdir_p!(dest) + + Enum.each(unzipped, fn {filename, data} -> + [_root | paths] = Path.split(filename) + path = Enum.join(paths, "/") + new_file_path = Path.join(dest, path) + + new_file_path + |> Path.dirname() + |> File.mkdir_p!() + + File.write!(new_file_path, data) + end) + else + {:ok, %{status: 404}} -> + {:error, "Bundle not found"} + + false -> + {:error, "Zip archive must contain \"dist\" folder"} + + error -> + {:error, error} + end + end + + defp build_frontend("admin", path) do + yarn = Pleroma.Config.get(:yarn, "yarn") + {_out, 0} = System.cmd(yarn, [], cd: path) + {_out, 0} = System.cmd(yarn, ["build:prod"], cd: path) + :ok + end + + defp build_frontend(_frontend, path) do + yarn = Pleroma.Config.get(:yarn, "yarn") + {_out, 0} = System.cmd(yarn, [], cd: path) + {_out, 0} = System.cmd(yarn, ["build"], cd: path) + :ok + end + + defp web_frontend_ref(frontend, @ref_develop) do + url = project_url(frontend) <> "/repository/branches" + + {:ok, %{status: 200, body: body}} = + Pleroma.HTTP.get(url, [], timeout: 120_000, recv_timeout: 120_000) + + json = Jason.decode!(body) + + %{"commit" => %{"short_id" => last_commit_ref}} = Enum.find(json, & &1["default"]) + + last_commit_ref + end + + # fallback to develop version if compatible stable ref is not defined in + # mix.exs for the given frontend + defp web_frontend_ref(frontend, @ref_stable) do + case Map.get(Pleroma.Application.frontends(), frontend) do + nil -> + web_frontend_ref(frontend, @ref_develop) + + ref -> + ref + end + end + + defp web_frontend_ref(_frontend, ref), do: ref + + defp project_url(frontend), + do: + "https://#{@pleroma_gitlab_host}/api/v4/projects/#{ + URI.encode_www_form(@frontends[frontend]["project"]) + }" + + defp archive_url(frontend, ref), + do: "https://#{@pleroma_gitlab_host}/#{@frontends[frontend]["project"]}/-/archive/#{ref}.zip" + + defp local_path_frontend_ref(path) do + path + |> Path.join("package.json") + |> File.read() + |> case do + {:ok, bin} -> + bin + |> Jason.decode!() + |> Map.get("version", @ref_local) + + _ -> + @ref_local + end + end + + defp post_install("mastodon", path) do + File.rename!("#{path}/assets/sw.js", "#{path}/sw.js") + + {:ok, files} = File.ls(path) + + Enum.each(files, fn file -> + with false <- file in ~w(packs sw.js) do + [path, file] + |> Path.join() + |> File.rm_rf!() + end + end) + end + + defp post_install(_frontend, _path) do + :ok + end + + defp install_frontend(frontend, source, dest) do + from = + case frontend do + "mastodon" -> + "public" + + "kenoma" -> + "build" + + _ -> + "dist" + end + + File.mkdir_p!(dest) + File.cp_r!(Path.join([source, from]), dest) + post_install(frontend, dest) + end +end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 91440b453..f57053a4c 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -33,7 +33,11 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: :string, static_dir: :string, listen_ip: :string, - listen_port: :string + listen_port: :string, + fe_primary: :string, + fe_mastodon: :string, + fe_admin: :string, + fe_static: :string ], aliases: [ o: :output, @@ -158,7 +162,85 @@ defmodule Mix.Tasks.Pleroma.Instance do ) |> Path.expand() - Config.put([:instance, :static_dir], static_dir) + install_fe = + case Mix.env() do + :test -> + fn _, _ -> "42" end + + _ -> + fn frontend, callback -> + case Pleroma.Utils.command_available?("yarn") do + false when frontend != "none" -> + message = + "To install #{frontend} frontend, `yarn` command is required. Please install it before continue. ([C]ontinue/[A]bort)" + + case String.downcase(shell_prompt(message, "C")) do + abort when abort in ["a", "abort"] -> + "none" + + _continue -> + callback.(frontend, callback) + end + + _ -> + Mix.Tasks.Pleroma.Frontend.run([ + "install", + frontend, + "--static-dir", + static_dir + ]) + end + end + end + + fe_primary = + get_option( + options, + :fe_primary, + "Choose primary frontend for your instance (available: pleroma/kenoma/none)", + "pleroma" + ) + + fe_primary_ref = install_fe.(fe_primary, install_fe) + + enable_static_fe? = + get_option( + options, + :fe_static, + "Would you like to enable Static frontend (render profiles and posts using server-generated HTML that is viewable without using JavaScript)?", + "y" + ) === "y" + + install_mastodon_fe? = + get_option( + options, + :fe_mastodon, + "Would you like to install Mastodon frontend?", + "y" + ) === "y" + + fe_mastodon_ref = + case install_mastodon_fe? do + true -> + install_fe.("mastodon", install_fe) + + false -> + "none" + end + + install_admin_fe? = + get_option( + options, + :fe_admin, + "Would you like to install Admin frontend?", + "y" + ) === "y" + + fe_admin_ref = + case install_admin_fe? do + true -> install_fe.("admin", install_fe) + false -> "none" + end secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) @@ -188,7 +270,11 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: uploads_dir, rum_enabled: rum_enabled, listen_ip: listen_ip, - listen_port: listen_port + listen_port: listen_port, + fe_primary: %{"name" => fe_primary, "ref" => fe_primary_ref}, + fe_mastodon: %{"name" => "mastodon", "ref" => fe_mastodon_ref}, + fe_admin: %{"name" => "admin", "ref" => fe_admin_ref}, + enable_static_fe?: enable_static_fe? ) result_psql = diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 01824aa18..ce7ba9b80 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -277,7 +277,7 @@ defmodule Mix.Tasks.Pleroma.User do shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " ")) url = - Pleroma.Web.Router.Helpers.redirect_url( + Pleroma.Web.Router.Helpers.frontend_url( Pleroma.Web.Endpoint, :registration_page, invite.token diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 0ffb55358..a220d2036 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -13,13 +13,17 @@ defmodule Pleroma.Application do @name Mix.Project.config()[:name] @version Mix.Project.config()[:version] + @stable? Mix.Project.config()[:stable?] @repository Mix.Project.config()[:source_url] + @frontends Mix.Project.config()[:frontends] @env Mix.env() def name, do: @name def version, do: @version + def stable?, do: @stable? def named_version, do: @name <> " " <> @version def repository, do: @repository + def frontends, do: @frontends def user_agent do case Config.get([:http, :user_agent], :default) do diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index dfadc10b3..d9cf2a6d7 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -47,7 +47,7 @@ defmodule Pleroma.Emails.UserEmail do to_name \\ nil ) do registration_url = - Router.Helpers.redirect_url( + Router.Helpers.frontend_url( Endpoint, :registration_page, user_invite_token.token diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex new file mode 100644 index 000000000..0db697996 --- /dev/null +++ b/lib/pleroma/frontend.ex @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Frontend do + @type primary_fe_opts :: %{config: Map.t(), controller: Module.t(), static: boolean()} + + @spec get_primary_fe_opts() :: primary_fe_opts() + def get_primary_fe_opts, + do: [:frontends] |> Pleroma.Config.get(%{}) |> Enum.into(%{}) |> get_primary_fe_opts() + + @spec get_primary_fe_opts(Map.t()) :: primary_fe_opts() + def get_primary_fe_opts(%{primary: %{"name" => "none"}} = fe_config) do + %{ + config: %{}, + controller: Pleroma.Web.Frontend.HeadlessController, + static: fe_config[:static] || false + } + end + + def get_primary_fe_opts(fe_config) do + %{ + config: fe_config[:primary], + controller: + Module.concat([ + Pleroma.Web.Frontend, + String.capitalize(fe_config[:primary]["name"]) <> "Controller" + ]), + static: fe_config[:static] || false + } + end + + @doc """ + Returns path to the requested file for the frontend from the given config. + If config is not provided, config for the `:primary` frontend is fetched and used. + If the requested file is not found for the frontend, the function fallback + to looking the file at instance static directory and then, in case of failure, + in priv/static directory. + Path returned in case of success is guaranteed to be of existing file. + """ + @spec fe_file_path(String.t(), map()) :: {:ok, String.t()} | {:error, String.t()} + def fe_file_path(filename, config \\ nil) do + %{"name" => name, "ref" => ref} = + with nil <- config do + get_primary_fe_opts()[:config] + end + + instance_base_path = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + frontend_path = Path.join([instance_base_path, "frontends", name, ref, filename]) + instance_path = Path.join([instance_base_path, filename]) + priv_path = Application.app_dir(:pleroma, ["priv", "static", filename]) + + cond do + File.exists?(instance_path) -> + {:ok, instance_path} + + File.exists?(frontend_path) -> + {:ok, frontend_path} + + File.exists?(priv_path) -> + {:ok, priv_path} + + true -> + {:error, + "File #{filename} was not found in #{inspect([instance_path, frontend_path, priv_path])}"} + end + end +end diff --git a/lib/pleroma/plugs/frontend_plug.ex b/lib/pleroma/plugs/frontend_plug.ex new file mode 100644 index 000000000..4db194dc3 --- /dev/null +++ b/lib/pleroma/plugs/frontend_plug.ex @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.FrontendPlug do + @moduledoc """ + Sets private key `:frontend` for the given connection. + It is set to one of admin|mastodon|primary frontends config values based + on `conn.request_path` + """ + + import Plug.Conn + + @behaviour Plug + + @mastodon_paths ~w(web packs sw.js api/web) + @admin_paths ~w(pleroma) + + def init(opts) do + opts + end + + for path <- @mastodon_paths do + def call(%{request_path: "/" <> unquote(path) <> _rest} = conn, _opts) do + fe_config = + Pleroma.Config.get([:frontends], %{mastodon: %{"name" => "mastodon", "ref" => ""}}) + + put_private(conn, :frontend, %{ + config: fe_config[:mastodon], + controller: Pleroma.Web.Frontend.MastodonController, + static: false + }) + end + end + + for path <- @admin_paths do + def call(%{request_path: "/" <> unquote(path) <> _rest} = conn, _opts) do + fe_config = Pleroma.Config.get([:frontends], %{admin: %{"name" => "admin", "ref" => ""}}) + + put_private(conn, :frontend, %{ + config: fe_config[:admin], + controller: Pleroma.Web.Frontend.AdminController, + static: false + }) + end + end + + def call(conn, _opts) do + put_private(conn, :frontend, Pleroma.Frontend.get_primary_fe_opts()) + end +end diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex index 7516f75c3..93e6528ea 100644 --- a/lib/pleroma/plugs/instance_static.ex +++ b/lib/pleroma/plugs/instance_static.ex @@ -9,19 +9,28 @@ defmodule Pleroma.Plugs.InstanceStatic do This is a shim to call `Plug.Static` but with runtime `from` configuration. Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones. + + Files in FE bundles can override files in priv/static, and files in + instance/static can override files in FE bundles: + instance/static > FE bundles > priv/static """ @behaviour Plug - def file_path(path) do - instance_path = - Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) + # list of paths to be looked up in intance/static + @instance_overridable_paths ~w(robots.txt emoji sounds images instance favicon.png) - if File.exists?(instance_path) do - instance_path - else - Path.join(Application.app_dir(:pleroma, "priv/static/"), path) - end - end + # both pleroma/{STATIC_PATH} and pleroma/admin/{STATIC_PATH} can be requested + @fe_prefixed_paths ~w(pleroma/admin pleroma) + @fe_paths [ + # mastodon + "packs", + "sw.js", + # primary frontend + "static", + "index.html", + "sw-pleroma.js", + "static-fe.css" + ] def init(opts) do opts @@ -30,29 +39,42 @@ defmodule Pleroma.Plugs.InstanceStatic do |> Plug.Static.init() end - for only <- Pleroma.Constants.static_only_files() do - at = Plug.Router.Utils.split("/") + # for only <- Pleroma.Constants.static_only_files() do + for path <- @fe_prefixed_paths do + def call(%{request_path: "/" <> unquote(path) <> _} = conn, opts) do + fe_path = get_fe_path(conn) + opts = %{opts | at: Plug.Router.Utils.split(unquote(path)), from: fe_path} + Plug.Static.call(conn, opts) + end + end - def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do - call_static( - conn, - opts, - unquote(at), - Pleroma.Config.get([:instance, :static_dir], "instance/static") - ) + for path <- @fe_paths do + def call(%{request_path: "/" <> unquote(path) <> _} = conn, opts) do + fe_path = get_fe_path(conn) + opts = %{opts | at: [], from: fe_path} + + with ^conn <- call_instance_static(conn, opts) do + Plug.Static.call(conn, opts) + end end end - def call(conn, _) do - conn + for path <- @instance_overridable_paths do + def call(%{request_path: "/" <> unquote(path) <> _} = conn, opts) do + call_instance_static(conn, opts) + end end - defp call_static(conn, opts, at, from) do - opts = - opts - |> Map.put(:from, from) - |> Map.put(:at, at) + def call(conn, _), do: conn + defp call_instance_static(conn, opts) do + instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") + opts = %{opts | at: [], from: instance_static_path} Plug.Static.call(conn, opts) end + + defp get_fe_path(%{private: %{frontend: %{config: conf}}}) do + instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") + Path.join([instance_static_path, "frontends", conf["name"], conf["ref"]]) + end end diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 143665c71..d3ae08835 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -4,23 +4,25 @@ defmodule Pleroma.Plugs.StaticFEPlug do import Plug.Conn - alias Pleroma.Web.StaticFE.StaticFEController def init(options), do: options - def call(conn, _) do - if enabled?() and requires_html?(conn) do + def call(%{private: %{frontend: %{static: true}}} = conn, _) do + action = Phoenix.Controller.action_name(conn) + + if requires_html?(conn) and has_action?(action) do conn - |> StaticFEController.call(:show) + |> Pleroma.Web.FrontendController.call(action) |> halt() else conn end end - defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) + def call(conn, _), do: conn - defp requires_html?(conn) do - Phoenix.Controller.get_format(conn) == "html" - end + defp requires_html?(conn), do: Phoenix.Controller.get_format(conn) == "html" + + defp has_action?(action), + do: function_exported?(Pleroma.Web.Frontend.StaticController, action, 2) end diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 6b8e3accf..fef9b6cc8 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -9,4 +9,33 @@ defmodule Pleroma.Utils do |> Enum.map(&Path.join(dir, &1)) |> Kernel.ParallelCompiler.compile() end + + @doc """ + POSIX-compliant check if command is available in the system + + ## Examples + iex> command_available?("git") + true + iex> command_available?("wrongcmd") + false + + """ + @spec command_available?(String.t()) :: boolean() + def command_available?(command) do + match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"])) + end + + @doc """ + Throws an exception in case required command is not available + """ + @spec command_required!(String.t()) :: :ok | no_return() + def command_required!(command) do + case command_available?(command) do + true -> + :ok + + false -> + raise "Command #{command} is required, but not available in $PATH" + end + end end diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex index 3a8380797..d9ffee454 100644 --- a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -109,7 +109,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do defp any do %Schema{ - oneOf: [ + anyOf: [ %Schema{type: :array}, %Schema{type: :object}, %Schema{type: :string}, diff --git a/lib/pleroma/web/controllers/frontend/admin_controller.ex b/lib/pleroma/web/controllers/frontend/admin_controller.ex new file mode 100644 index 000000000..c6af4811a --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/admin_controller.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.AdminController do + use Pleroma.Web, :controller + use Pleroma.Web.Frontend.DefaultController +end diff --git a/lib/pleroma/web/controllers/frontend/default_controller.ex b/lib/pleroma/web/controllers/frontend/default_controller.ex new file mode 100644 index 000000000..e9aa59330 --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/default_controller.ex @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.DefaultController do + defmacro __using__(_opts) do + quote do + import Pleroma.Frontend, only: [fe_file_path: 1, fe_file_path: 2] + + require Logger + + def index(conn, _params) do + status = conn.status || 200 + + {:ok, index_file_path} = fe_file_path("index.html", conn.private[:frontend][:config]) + + conn + |> put_resp_content_type("text/html") + |> send_file(status, index_file_path) + end + + def index_with_meta(conn, params) do + index_with_generated_data(conn, params, [:metadata, :preload]) + end + + def index_with_preload(conn, params) do + index_with_generated_data(conn, params, [:preload]) + end + + defp index_with_generated_data(conn, params, generators) do + {:ok, path} = fe_file_path("index.html") + {:ok, index_content} = File.read(path) + + generated = + Enum.reduce(generators, "", fn generator, acc -> + acc <> generate_data(conn, params, generator) + end) + + response = String.replace(index_content, "<!--server-generated-meta-->", generated) + + html(conn, response) + end + + def api_not_implemented(conn, _params) do + conn + |> put_status(404) + |> json(%{error: "Not implemented"}) + end + + def empty(conn, _params) do + conn + |> put_status(204) + |> text("") + end + + def fallback(conn, _params) do + conn + |> put_status(404) + |> text("Not found") + end + + defp generate_data(conn, params, :preload) do + try do + Pleroma.Web.Preload.build_tags(conn, params) + rescue + e -> + Logger.error( + "Preloading for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end + + defp generate_data(conn, params, :metadata) do + try do + Pleroma.Web.Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end + + defoverridable index: 2, + index_with_meta: 2, + index_with_preload: 2, + api_not_implemented: 2, + empty: 2, + fallback: 2 + end + end +end diff --git a/lib/pleroma/web/controllers/frontend/headless_controller.ex b/lib/pleroma/web/controllers/frontend/headless_controller.ex new file mode 100644 index 000000000..781811a66 --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/headless_controller.ex @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.HeadlessController do + use Pleroma.Web, :controller + + def index_with_preload(conn, _params) do + conn + |> put_status(404) + |> text("") + end +end diff --git a/lib/pleroma/web/controllers/frontend/kenoma_controller.ex b/lib/pleroma/web/controllers/frontend/kenoma_controller.ex new file mode 100644 index 000000000..bb4f7f550 --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/kenoma_controller.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.KenomaController do + use Pleroma.Web, :controller + use Pleroma.Web.Frontend.DefaultController +end diff --git a/lib/pleroma/web/controllers/frontend/mastodon_controller.ex b/lib/pleroma/web/controllers/frontend/mastodon_controller.ex new file mode 100644 index 000000000..224f19d92 --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/mastodon_controller.ex @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.MastodonController do + use Pleroma.Web, :controller + use Pleroma.Web.Frontend.DefaultController + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + + plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) + + # Note: :index action handles attempt of unauthenticated access to private instance with redirect + plug( + OAuthScopesPlug, + %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true} + when action == :index + ) + + plug( + Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + when action != :index + ) + + def index(%{assigns: %{user: user, token: token}} = conn, _params) + when not is_nil(user) and not is_nil(token) do + conn + |> put_layout(false) + |> render("index.html", + token: token.token, + user: user, + custom_emojis: Pleroma.Emoji.get_all() + ) + end + + def index(conn, _params) do + conn + |> put_session(:return_to, conn.request_path) + |> redirect(to: auth_path(conn, :login)) + end + + @doc "GET /web/manifest.json" + def manifest(conn, _params) do + render(conn, "manifest.json") + end + + @doc "PUT /api/web/settings" + def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do + case User.mastodon_settings_update(user, settings) do + {:ok, _} -> + json(conn, %{}) + + e -> + conn + |> put_status(:internal_server_error) + |> json(%{error: inspect(e)}) + end + end +end diff --git a/lib/pleroma/web/controllers/frontend/pleroma_controller.ex b/lib/pleroma/web/controllers/frontend/pleroma_controller.ex new file mode 100644 index 000000000..3c106f1de --- /dev/null +++ b/lib/pleroma/web/controllers/frontend/pleroma_controller.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.PleromaController do + use Pleroma.Web, :controller + use Pleroma.Web.Frontend.DefaultController + + alias Pleroma.User + + def index_with_meta_and_user(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do + case User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do + %User{} = user -> + index_with_meta(conn, %{user: user}) + + _ -> + index(conn, params) + end + end + + defdelegate registration_page(conn, params), to: __MODULE__, as: :index +end diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/controllers/frontend/static_controller.ex index a7a891b13..27d70d3da 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/controllers/frontend/static_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.StaticFE.StaticFEController do +defmodule Pleroma.Web.Frontend.StaticController do use Pleroma.Web, :controller alias Pleroma.Activity @@ -11,11 +11,8 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Metadata - alias Pleroma.Web.Router.Helpers plug(:put_layout, :static_fe) - plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) - plug(:assign_id) plug(Pleroma.Plugs.EnsureAuthenticatedPlug, unless_func: &Pleroma.Web.FederatingPlug.federating?/1 @@ -23,65 +20,20 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do @page_keys ["max_id", "min_id", "limit", "since_id", "order"] - defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), - do: name - - defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary), - do: summary - - defp get_title(_), do: nil - - defp not_found(conn, message) do - conn - |> put_status(404) - |> render("error.html", %{message: message, meta: ""}) - end - - defp get_counts(%Activity{} = activity) do - %Object{data: data} = Object.normalize(activity) - - %{ - likes: data["like_count"] || 0, - replies: data["repliesCount"] || 0, - announces: data["announcement_count"] || 0 - } - end - - defp represent(%Activity{} = activity), do: represent(activity, false) - - defp represent(%Activity{object: %Object{data: data}} = activity, selected) do - {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) + def object(conn, %{"uuid" => _uuid}) do + url = url(conn) <> conn.request_path - link = - case user.local do - true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) - _ -> data["url"] || data["external_url"] || data["id"] - end - - content = - if data["content"] do - data["content"] - |> Pleroma.HTML.filter_tags() - |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) - else - nil - end + case Activity.get_create_by_object_ap_id_with_object(url) do + %Activity{} = activity -> + to = o_status_path(Pleroma.Web.Endpoint, :notice, activity) + redirect(conn, to: to) - %{ - user: User.sanitize_html(user), - title: get_title(activity.object), - content: content, - attachment: data["attachment"], - link: link, - published: data["published"], - sensitive: data["sensitive"], - selected: selected, - counts: get_counts(activity), - id: activity.id - } + _ -> + not_found(conn, "Post not found.") + end end - def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do + def notice(conn, %{"id" => notice_id}) do with %Activity{local: true} = activity <- Activity.get_by_id_with_object(notice_id), true <- Visibility.is_public?(activity.object), @@ -106,7 +58,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do end end - def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do + def feed_redirect(conn, %{"nickname" => username_or_id} = params) do case User.get_cached_by_nickname_or_id(username_or_id) do %User{} = user -> meta = Metadata.build_tags(%{user: user}) @@ -140,12 +92,12 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do end end - def show(%{assigns: %{object_id: _}} = conn, _params) do - url = Helpers.url(conn) <> conn.request_path + def activity(conn, %{"uuid" => _uuid}) do + url = url(conn) <> conn.request_path - case Activity.get_create_by_object_ap_id_with_object(url) do + case Activity.get_by_ap_id(url) do %Activity{} = activity -> - to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) + to = o_status_path(Pleroma.Web.Endpoint, :notice, activity) redirect(conn, to: to) _ -> @@ -153,30 +105,60 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do end end - def show(%{assigns: %{activity_id: _}} = conn, _params) do - url = Helpers.url(conn) <> conn.request_path + defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), + do: name - case Activity.get_by_ap_id(url) do - %Activity{} = activity -> - to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) - redirect(conn, to: to) + defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary), + do: summary - _ -> - not_found(conn, "Post not found.") - end + defp get_title(_), do: nil + + defp not_found(conn, message) do + conn + |> put_status(404) + |> render("error.html", %{message: message, meta: ""}) + end + + defp get_counts(%Activity{} = activity) do + %Object{data: data} = Object.normalize(activity) + + %{ + likes: data["like_count"] || 0, + replies: data["repliesCount"] || 0, + announces: data["announcement_count"] || 0 + } end - defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), - do: assign(conn, :notice_id, notice_id) + defp represent(%Activity{} = activity), do: represent(activity, false) - defp assign_id(%{path_info: ["users", user_id]} = conn, _opts), - do: assign(conn, :username_or_id, user_id) + defp represent(%Activity{object: %Object{data: data}} = activity, selected) do + {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) - defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts), - do: assign(conn, :object_id, object_id) + link = + if user.local do + o_status_url(Pleroma.Web.Endpoint, :notice, activity) + else + data["url"] || data["external_url"] || data["id"] + end - defp assign_id(%{path_info: ["activities", activity_id]} = conn, _opts), - do: assign(conn, :activity_id, activity_id) + content = + if data["content"] do + data["content"] + |> Pleroma.HTML.filter_tags() + |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) + end - defp assign_id(conn, _opts), do: conn + %{ + user: User.sanitize_html(user), + title: get_title(activity.object), + content: content, + attachment: data["attachment"], + link: link, + published: data["published"], + sensitive: data["sensitive"], + selected: selected, + counts: get_counts(activity), + id: activity.id + } + end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 226d42c2c..a4476ffb7 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.Endpoint do plug(CORSPlug) plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Plugs.UploadedMedia) + plug(Pleroma.Plugs.FrontendPlug) @static_cache_control "public, no-cache" @@ -36,6 +37,8 @@ defmodule Pleroma.Web.Endpoint do Plug.Static, at: "/", from: :pleroma, + # only: + # ~w(robots.txt static-fe.css finmoji emoji sounds images instance favicon.png schemas doc), only: Pleroma.Constants.static_only_files(), # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength gzip: true, @@ -45,13 +48,6 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") - - plug(Plug.Static, - at: "/pleroma/admin/", - from: {:pleroma, "priv/static/adminfe/"} - ) - # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex deleted file mode 100644 index 431ad5485..000000000 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ /dev/null @@ -1,108 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Fallback.RedirectController do - use Pleroma.Web, :controller - - require Logger - - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Preload - - def api_not_implemented(conn, _params) do - conn - |> put_status(404) - |> json(%{error: "Not implemented"}) - end - - def redirector(conn, _params, code \\ 200) do - conn - |> put_resp_content_type("text/html") - |> send_file(code, index_file_path()) - end - - def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do - redirector_with_meta(conn, %{user: user}) - else - nil -> - redirector(conn, params) - end - end - - def redirector_with_meta(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - - tags = build_tags(conn, params) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", tags <> preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do - redirect(conn, to: "/pleroma/admin/") - end - - def redirector_with_preload(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def registration_page(conn, params) do - redirector(conn, params) - end - - def empty(conn, _params) do - conn - |> put_status(204) - |> text("") - end - - defp index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") - end - - defp build_tags(conn, params) do - try do - Metadata.build_tags(params) - rescue - e -> - Logger.error( - "Metadata rendering for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end - - defp preload_data(conn, params) do - try do - Preload.build_tags(conn, params) - rescue - e -> - Logger.error( - "Preloading for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end -end diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index d56f43818..8a7e94b94 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.Feed.UserController do use Pleroma.Web, :controller - alias Fallback.RedirectController alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPubController @@ -13,11 +12,15 @@ defmodule Pleroma.Web.Feed.UserController do plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) + plug(Pleroma.Plugs.StaticFEPlug) + action_fallback(:errors) def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do - RedirectController.redirector_with_meta(conn, %{user: user}) + conn + |> Map.put(:params, %{user: user}) + |> Pleroma.Web.FrontendController.call(:index_with_meta) end end diff --git a/lib/pleroma/web/frontend_controller.ex b/lib/pleroma/web/frontend_controller.ex new file mode 100644 index 000000000..f4b8c427b --- /dev/null +++ b/lib/pleroma/web/frontend_controller.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FrontendController do + use Pleroma.Web, :controller + import Pleroma.Frontend, only: [get_primary_fe_opts: 0] + alias Pleroma.Web.Frontend.StaticController + + def action(conn, _opts) do + # `conn.private[:frontend]` can be missing if the function is called outside + # of the standard controller pipeline. In this case we set frontend as a + # :primary one + fe_config = conn.private[:frontend] || get_primary_fe_opts() + + # can only be true for :primary frontend + static_enabled? = Map.get(fe_config, :static, false) + + action_name = action_name(conn) + + {controller, action} = + cond do + static_enabled? and function_exported?(StaticController, action_name, 2) -> + {StaticController, action_name} + + function_exported?(fe_config[:controller], action_name, 2) -> + {fe_config[:controller], action_name} + + true -> + {fe_config[:controller], :fallback} + end + + conn + # in case we are serving an internal call + |> put_private(:frontend, fe_config) + |> put_view(Phoenix.Controller.__view__(controller)) + |> controller.call(controller.init(action)) + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 753b3db3e..1404fcad9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -51,8 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do @doc "DELETE /auth/sign_out" def logout(conn, _) do conn - |> clear_session - |> redirect(to: "/") + |> clear_session() + |> redirect(to: frontend_path(conn, :index_with_preload, [])) end @doc "POST /auth/password" @@ -75,7 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do defp local_mastodon_root_path(conn) do case get_session(conn, :return_to) do nil -> - masto_fe_path(conn, :index, ["getting-started"]) + frontend_mastodon_path(conn, :index, ["getting-started"]) return_to -> delete_session(conn, :return_to) diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index de1b0b3f0..08bb47aa8 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do use Pleroma.Web, :controller - alias Fallback.RedirectController alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Plugs.RateLimiter @@ -30,6 +29,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do when action in [:object, :activity, :notice] ) + plug(Pleroma.Plugs.StaticFEPlug) + action_fallback(:errors) def object(%{assigns: %{format: format}} = conn, _params) @@ -37,14 +38,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do ActivityPubController.call(conn, :object) end - def object(%{assigns: %{format: format}} = conn, _params) do + def object(%{assigns: %{format: _format}} = conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + redirect(conn, to: o_status_path(conn, :notice, activity.id)) else reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} @@ -59,13 +58,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do ActivityPubController.call(conn, :activity) end - def activity(%{assigns: %{format: format}} = conn, _params) do + def activity(%{assigns: %{format: _format}} = conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + redirect(conn, to: o_status_path(conn, :notice, activity.id)) else reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} @@ -91,24 +88,25 @@ defmodule Pleroma.Web.OStatus.OStatusController do activity.data["type"] == "Create" -> %Object{} = object = Object.normalize(activity) - RedirectController.redirector_with_meta( - conn, - %{ - activity_id: activity.id, - object: object, - url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), - user: user - } - ) + params = %{ + activity_id: activity.id, + object: object, + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + user: user + } + + conn + |> Map.put(:params, params) + |> Pleroma.Web.FrontendController.call(:index_with_meta) true -> - RedirectController.redirector(conn, nil) + Pleroma.Web.FrontendController.call(conn, :index) end else reason when reason in [{:public?, false}, {:activity, nil}] -> conn |> put_status(404) - |> RedirectController.redirector(nil, 404) + |> Pleroma.Web.FrontendController.call(:index) e -> e @@ -135,7 +133,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do _error -> conn |> put_status(404) - |> RedirectController.redirector(nil, 404) + |> Pleroma.Web.FrontendController.call(:index) end end diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex index 50d1f3382..05bdb4fdb 100644 --- a/lib/pleroma/web/preload/instance.ex +++ b/lib/pleroma/web/preload/instance.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Preload.Providers.Instance do - alias Pleroma.Plugs.InstanceStatic alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Preload.Providers.Provider @@ -28,13 +27,15 @@ defmodule Pleroma.Web.Preload.Providers.Instance do end defp build_panel_tag(acc) do - instance_path = InstanceStatic.file_path(@panel_url |> to_string()) + panel_file = Path.basename(@panel_url) - if File.exists?(instance_path) do - panel_data = File.read!(instance_path) - Map.put(acc, @panel_url, panel_data) - else - acc + case Pleroma.Frontend.fe_file_path(panel_file) do + {:ok, instance_path} -> + panel_data = File.read!(instance_path) + Map.put(acc, @panel_url, panel_data) + + {:error, _e} -> + acc end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 386308362..639581784 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -464,7 +464,8 @@ defmodule Pleroma.Web.Router do pipe_through(:authenticated_api) # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere - put("/settings", MastoFEController, :put_settings) + # put("/settings", MastoFEController, :put_settings) + put("/settings", FrontendController, :put_settings, as: :frontend_mastodon) end scope "/api/v1", Pleroma.Web.MastodonAPI do @@ -547,7 +548,6 @@ defmodule Pleroma.Web.Router do pipeline :ostatus do plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) - plug(Pleroma.Plugs.StaticFEPlug) end pipeline :oembed do @@ -652,7 +652,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do pipe_through(:api) - get("/web/manifest.json", MastoFEController, :manifest) + get("/web/manifest.json", FrontendController, :manifest, as: :frontend_mastodon) end scope "/", Pleroma.Web do @@ -663,8 +663,7 @@ defmodule Pleroma.Web.Router do post("/auth/password", MastodonAPI.AuthController, :password_reset) - get("/web/*path", MastoFEController, :index) - + get("/web/*path", FrontendController, :index, as: :frontend_mastodon) get("/embed/:id", EmbedController, :show) end @@ -714,12 +713,16 @@ defmodule Pleroma.Web.Router do get("/check_password", MongooseIMController, :check_password) end - scope "/", Fallback do - get("/registration/:token", RedirectController, :registration_page) - get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) - get("/api*path", RedirectController, :api_not_implemented) - get("/*path", RedirectController, :redirector_with_preload) + scope "/pleroma/admin", Pleroma.Web do + get("/*path", FrontendController, :index, as: :frontend_admin) + end + + scope "/", Pleroma.Web do + get("/registration/:token", FrontendController, :registration_page) + get("/:maybe_nickname_or_id", FrontendController, :index_with_meta_and_user) + get("/api*path", FrontendController, :api_not_implemented) + get("/*path", FrontendController, :index_with_preload) - options("/*path", RedirectController, :empty) + options("/*path", FrontendController, :empty) end end diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/frontend/mastodon/index.html.eex index c330960fa..22f3f3b5a 100644 --- a/lib/pleroma/web/templates/masto_fe/index.html.eex +++ b/lib/pleroma/web/templates/frontend/mastodon/index.html.eex @@ -6,8 +6,8 @@ <title> <%= Config.get([:instance, :name]) %> </title> -<link rel="icon" type="image/png" href="/favicon.png"/> -<link rel="manifest" type="applicaton/manifest+json" href="<%= masto_fe_path(Pleroma.Web.Endpoint, :manifest) %>" /> +<link rel="icon" type="image/png" href="favicon.png"/> +<link rel="manifest" type="applicaton/manifest+json" href="<%= frontend_mastodon_path(Pleroma.Web.Endpoint, :manifest) %>" /> <meta name="theme-color" content="<%= Config.get([:manifest, :theme_color]) %>" /> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/frontend/static/_attachment.html.eex index 4853e7f4b..4853e7f4b 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex +++ b/lib/pleroma/web/templates/frontend/static/_attachment.html.eex diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/frontend/static/_notice.html.eex index df0244795..07c80d72b 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex +++ b/lib/pleroma/web/templates/frontend/static/_notice.html.eex @@ -19,7 +19,7 @@ <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %> <%= if @sensitive do %> <details class="nsfw"> - <summary><%= Gettext.gettext("sensitive media") %></summary> + <summary><%= gettext("sensitive media") %></summary> <div> <%= render("_attachment.html", %{name: name, url: url["href"], mediaType: fetch_media_type(url)}) %> @@ -33,9 +33,9 @@ </div> <%= if @selected do %> <dl class="counts"> - <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd> - <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd> - <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd> + <dt><%= gettext("replies") %></dt><dd><%= @counts.replies %></dd> + <dt><%= gettext("announces") %></dt><dd><%= @counts.announces %></dd> + <dt><%= gettext("likes") %></dt><dd><%= @counts.likes %></dd> </dl> <% end %> </div> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/frontend/static/_user_card.html.eex index 977b894d3..c29170ff5 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex +++ b/lib/pleroma/web/templates/frontend/static/_user_card.html.eex @@ -1,10 +1,10 @@ <div class="p-author h-card"> <a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>"> <div class="avatar"> - <img class="u-photo" src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt=""> + <img class="u-photo" src="<%= User.avatar_url(@user) |> Pleroma.Web.MediaProxy.url %>" width="48" height="48" alt=""> </div> <span class="display-name"> - <bdi class="p-name"><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi> + <bdi class="p-name"><%= raw Pleroma.Emoji.Formatter.emojify(@user.name, @user.emoji) %></bdi> <span class="nickname"><%= @user.nickname %></span> </span> </a> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/frontend/static/conversation.html.eex index 2acd84828..2acd84828 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex +++ b/lib/pleroma/web/templates/frontend/static/conversation.html.eex diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/frontend/static/error.html.eex index d98a1eba7..d98a1eba7 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex +++ b/lib/pleroma/web/templates/frontend/static/error.html.eex diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/frontend/static/profile.html.eex index 3191bf450..1690c5edf 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/frontend/static/profile.html.eex @@ -2,12 +2,12 @@ <h1><%= link instance_name(), to: "/" %></h1> <h3> - <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>"> + <form class="pull-right collapse" method="POST" action="<%= util_path(@conn, :remote_subscribe) %>"> <input type="hidden" name="nickname" value="<%= @user.nickname %>"> <input type="hidden" name="profile" value=""> <button type="submit" class="collapse">Remote follow</button> </form> - <%= raw Formatter.emojify(@user.name, @user.emoji) %> | + <%= raw Pleroma.Emoji.Formatter.emojify(@user.name, @user.emoji) %> | <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %> </h3> <p><%= raw @user.bio %></p> diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex index dc0ee2a5c..b7ec4d0c9 100644 --- a/lib/pleroma/web/templates/layout/static_fe.html.eex +++ b/lib/pleroma/web/templates/layout/static_fe.html.eex @@ -5,7 +5,7 @@ <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> <title><%= Pleroma.Config.get([:instance, :name]) %></title> <%= Phoenix.HTML.raw(assigns[:meta] || "") %> - <link rel="stylesheet" href="/static-fe/static-fe.css"> + <link rel="stylesheet" href="<%= static_path(@conn, "/static-fe.css") %>"/> </head> <body> <div class="container"> diff --git a/lib/pleroma/web/views/frontend/mastodon_view.ex b/lib/pleroma/web/views/frontend/mastodon_view.ex new file mode 100644 index 000000000..3db8a934e --- /dev/null +++ b/lib/pleroma/web/views/frontend/mastodon_view.ex @@ -0,0 +1,121 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Frontend.MastodonView do + use Pleroma.Web, :view + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.CustomEmojiView + + @default_settings %{ + onboarded: true, + home: %{ + shows: %{ + reblog: true, + reply: true + } + }, + notifications: %{ + alerts: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + shows: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + }, + sounds: %{ + follow: true, + favourite: true, + reblog: true, + mention: true + } + } + } + + def initial_state(token, user, custom_emojis) do + limit = Config.get([:instance, :limit]) + + %{ + meta: %{ + streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), + access_token: token, + locale: "en", + domain: Pleroma.Web.Endpoint.host(), + admin: "1", + me: "#{user.id}", + unfollow_modal: false, + boost_modal: false, + delete_modal: true, + auto_play_gif: false, + display_sensitive_media: false, + reduce_motion: false, + max_toot_chars: limit, + mascot: User.get_mascot(user)["url"] + }, + poll_limits: Config.get([:instance, :poll_limits]), + rights: %{ + delete_others_notice: present?(user.is_moderator), + admin: present?(user.is_admin) + }, + compose: %{ + me: "#{user.id}", + default_privacy: user.default_scope, + default_sensitive: false, + allow_content_types: Config.get([:instance, :allowed_post_formats]) + }, + media_attachments: %{ + accept_content_types: [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webm", + ".mp4", + ".m4v", + "image\/jpeg", + "image\/png", + "image\/gif", + "video\/webm", + "video\/mp4" + ] + }, + settings: Map.get(user, :settings, @default_settings), + push_subscription: nil, + accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)}, + custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis), + char_limit: limit + } + |> Jason.encode!() + |> Phoenix.HTML.raw() + end + + defp present?(nil), do: false + defp present?(false), do: false + defp present?(_), do: true + + def render("manifest.json", _params) do + %{ + name: Config.get([:instance, :name]), + description: Config.get([:instance, :description]), + icons: Config.get([:manifest, :icons]), + theme_color: Config.get([:manifest, :theme_color]), + background_color: Config.get([:manifest, :background_color]), + display: "standalone", + scope: Pleroma.Web.base_url(), + start_url: frontend_mastodon_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), + categories: [ + "social" + ], + serviceworker: %{ + src: "/sw.js" + } + } + end +end diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/views/frontend/static_view.ex index b3d1d1ec8..4cdb1128a 100644 --- a/lib/pleroma/web/static_fe/static_fe_view.ex +++ b/lib/pleroma/web/views/frontend/static_view.ex @@ -2,17 +2,12 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.StaticFE.StaticFEView do +defmodule Pleroma.Web.Frontend.StaticView do use Pleroma.Web, :view alias Calendar.Strftime - alias Pleroma.Emoji.Formatter alias Pleroma.User - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Gettext - alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router.Helpers use Phoenix.HTML diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index f739dacb6..d11ecd4f4 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -109,7 +109,7 @@ defmodule Pleroma.Web.MastoFEView do background_color: Config.get([:manifest, :background_color]), display: "standalone", scope: Pleroma.Web.base_url(), - start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), + start_url: frontend_mastodon_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), categories: [ "social" ], diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 4f9281851..69aebd877 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -120,6 +120,8 @@ defmodule Pleroma.Web do conn end end + + defoverridable(action: 2) end end @@ -138,6 +140,8 @@ defmodule Pleroma.Web do require Logger + alias Pleroma.Web.Endpoint + @doc "Same as `render/3` but wrapped in a rescue block" def safe_render(view, template, assigns \\ %{}) do Phoenix.View.render(view, template, assigns) |