diff options
Diffstat (limited to 'lib')
79 files changed, 1782 insertions, 653 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex new file mode 100644 index 000000000..1b758ea33 --- /dev/null +++ b/lib/mix/pleroma.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Pleroma do +  @doc "Common functions to be reused in mix tasks" +  def start_pleroma do +    Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) +    {:ok, _} = Application.ensure_all_started(:pleroma) +  end + +  def load_pleroma do +    Application.load(:pleroma) +  end + +  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do +    Keyword.get(options, opt) || shell_prompt(prompt, defval, defname) +  end + +  def shell_prompt(prompt, defval \\ nil, defname \\ nil) do +    prompt_message = "#{prompt} [#{defname || defval}] " + +    input = +      if mix_shell?(), +        do: Mix.shell().prompt(prompt_message), +        else: :io.get_line(prompt_message) + +    case input do +      "\n" -> +        case defval do +          nil -> +            shell_prompt(prompt, defval, defname) + +          defval -> +            defval +        end + +      input -> +        String.trim(input) +    end +  end + +  def shell_yes?(message) do +    if mix_shell?(), +      do: Mix.shell().yes?("Continue?"), +      else: shell_prompt(message, "Continue?") in ~w(Yn Y y) +  end + +  def shell_info(message) do +    if mix_shell?(), +      do: Mix.shell().info(message), +      else: IO.puts(message) +  end + +  def shell_error(message) do +    if mix_shell?(), +      do: Mix.shell().error(message), +      else: IO.puts(:stderr, message) +  end + +  @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)" +  def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0) + +  def escape_sh_path(path) do +    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') +  end +end diff --git a/lib/mix/tasks/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index e4b1a638a..d43db7b35 100644 --- a/lib/mix/tasks/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -1,9 +1,9 @@  defmodule Mix.Tasks.Pleroma.Benchmark do +  import Mix.Pleroma    use Mix.Task -  alias Mix.Tasks.Pleroma.Common    def run(["search"]) do -    Common.start_pleroma() +    start_pleroma()      Benchee.run(%{        "search" => fn -> @@ -13,7 +13,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do    end    def run(["tag"]) do -    Common.start_pleroma() +    start_pleroma()      Benchee.run(%{        "tag" => fn -> diff --git a/lib/mix/tasks/pleroma/common.ex b/lib/mix/tasks/pleroma/common.ex deleted file mode 100644 index 48c0c1346..000000000 --- a/lib/mix/tasks/pleroma/common.ex +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.Common do -  @doc "Common functions to be reused in mix tasks" -  def start_pleroma do -    Mix.Task.run("app.start") -  end - -  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do -    Keyword.get(options, opt) || -      case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do -        "\n" -> -          case defval do -            nil -> get_option(options, opt, prompt, defval) -            defval -> defval -          end - -        opt -> -          opt |> String.trim() -      end -  end - -  def escape_sh_path(path) do -    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') -  end -end diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex new file mode 100644 index 000000000..faa605d9b --- /dev/null +++ b/lib/mix/tasks/pleroma/config.ex @@ -0,0 +1,79 @@ +defmodule Mix.Tasks.Pleroma.Config do +  use Mix.Task +  import Mix.Pleroma +  alias Pleroma.Repo +  alias Pleroma.Web.AdminAPI.Config +  @shortdoc "Manages the location of the config" +  @moduledoc """ +  Manages the location of the config. + +  ## Transfers config from file to DB. + +      mix pleroma.config migrate_to_db + +  ## Transfers config from DB to file. + +      mix pleroma.config migrate_from_db ENV +  """ + +  def run(["migrate_to_db"]) do +    start_pleroma() + +    if Pleroma.Config.get([:instance, :dynamic_configuration]) do +      Application.get_all_env(:pleroma) +      |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) +      |> Enum.each(fn {k, v} -> +        key = to_string(k) |> String.replace("Elixir.", "") +        {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v}) +        Mix.shell().info("#{key} is migrated.") +      end) + +      Mix.shell().info("Settings migrated.") +    else +      Mix.shell().info( +        "Migration is not allowed by config. You can change this behavior in instance settings." +      ) +    end +  end + +  def run(["migrate_from_db", env, delete?]) do +    start_pleroma() + +    delete? = if delete? == "true", do: true, else: false + +    if Pleroma.Config.get([:instance, :dynamic_configuration]) do +      config_path = "config/#{env}.exported_from_db.secret.exs" + +      {:ok, file} = File.open(config_path, [:write]) +      IO.write(file, "use Mix.Config\r\n") + +      Repo.all(Config) +      |> Enum.each(fn config -> +        mark = +          if String.starts_with?(config.key, "Pleroma.") or +               String.starts_with?(config.key, "Ueberauth"), +             do: ",", +             else: ":" + +        IO.write( +          file, +          "config :#{config.group}, #{config.key}#{mark} #{ +            inspect(Config.from_binary(config.value)) +          }\r\n" +        ) + +        if delete? do +          {:ok, _} = Repo.delete(config) +          Mix.shell().info("#{config.key} deleted from DB.") +        end +      end) + +      File.close(file) +      System.cmd("mix", ["format", config_path]) +    else +      Mix.shell().info( +        "Migration is not allowed by config. You can change this behavior in instance settings." +      ) +    end +  end +end diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 4d480ac3f..e91fb31d1 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -3,12 +3,12 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Mix.Tasks.Pleroma.Database do -  alias Mix.Tasks.Pleroma.Common    alias Pleroma.Conversation    alias Pleroma.Object    alias Pleroma.Repo    alias Pleroma.User    require Logger +  import Mix.Pleroma    use Mix.Task    @shortdoc "A collection of database related tasks" @@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Database do          ]        ) -    Common.start_pleroma() +    start_pleroma()      Logger.info("Removing embedded objects")      Repo.query!( @@ -66,12 +66,12 @@ defmodule Mix.Tasks.Pleroma.Database do    end    def run(["bump_all_conversations"]) do -    Common.start_pleroma() +    start_pleroma()      Conversation.bump_for_all_activities()    end    def run(["update_users_following_followers_counts"]) do -    Common.start_pleroma() +    start_pleroma()      users = Repo.all(User)      Enum.each(users, &User.remove_duplicated_following/1) @@ -89,7 +89,7 @@ defmodule Mix.Tasks.Pleroma.Database do          ]        ) -    Common.start_pleroma() +    start_pleroma()      deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 7ac3df5c7..19c4ce71e 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,6 +1,5 @@  defmodule Mix.Tasks.Pleroma.Digest do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common    @shortdoc "Manages digest emails"    @moduledoc """ @@ -14,7 +13,7 @@ defmodule Mix.Tasks.Pleroma.Digest do    Example: ``mix pleroma.digest test donaldtheduck 2019-05-20``    """    def run(["test", nickname | opts]) do -    Common.start_pleroma() +    Mix.Pleroma.start_pleroma()      user = Pleroma.User.get_by_nickname(nickname) diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex new file mode 100644 index 000000000..324f57fdd --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/ecto.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl +defmodule Mix.Tasks.Pleroma.Ecto do +  @doc """ +  Ensures the given repository's migrations path exists on the file system. +  """ +  @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() +  def ensure_migrations_path(repo, opts) do +    path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") + +    path = +      case Path.type(path) do +        :relative -> +          Path.join(Application.app_dir(:pleroma), path) + +        :absolute -> +          path +      end + +    if not File.dir?(path) do +      raise_missing_migrations(Path.relative_to_cwd(path), repo) +    end + +    path +  end + +  @doc """ +  Returns the private repository path relative to the source. +  """ +  def source_repo_priv(repo) do +    config = repo.config() +    priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" +    Path.join(Application.app_dir(:pleroma), priv) +  end + +  defp raise_missing_migrations(path, repo) do +    raise(""" +    Could not find migrations directory #{inspect(path)} +    for repo #{inspect(repo)}. +    This may be because you are in a new project and the +    migration directory has not been created yet. Creating an +    empty directory at the path above will fix this error. +    If you expected existing migrations to be found, please +    make sure your repository has been properly configured +    and the configured path exists. +    """) +  end +end diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex new file mode 100644 index 000000000..855c977f6 --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/migrate.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.Migrate do +  use Mix.Task +  import Mix.Pleroma +  require Logger + +  @shortdoc "Wrapper on `ecto.migrate` task." + +  @aliases [ +    n: :step, +    v: :to +  ] + +  @switches [ +    all: :boolean, +    step: :integer, +    to: :integer, +    quiet: :boolean, +    log_sql: :boolean, +    strict_version_order: :boolean, +    migrations_path: :string +  ] + +  @moduledoc """ +  Changes `Logger` level to `:info` before start migration. +  Changes level back when migration ends. + +  ## Start migration + +      mix pleroma.ecto.migrate [OPTIONS] + +  Options: +    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Migrate.html +  """ + +  @impl true +  def run(args \\ []) do +    load_pleroma() +    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + +    opts = +      if opts[:to] || opts[:step] || opts[:all], +        do: opts, +        else: Keyword.put(opts, :all, true) + +    opts = +      if opts[:quiet], +        do: Keyword.merge(opts, log: false, log_sql: false), +        else: opts + +    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts) + +    level = Logger.level() +    Logger.configure(level: :info) + +    {:ok, _, _} = Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :up, opts)) + +    Logger.configure(level: level) +  end +end diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex new file mode 100644 index 000000000..2ffb0901c --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto/rollback.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.Rollback do +  use Mix.Task +  import Mix.Pleroma +  require Logger +  @shortdoc "Wrapper on `ecto.rollback` task" + +  @aliases [ +    n: :step, +    v: :to +  ] + +  @switches [ +    all: :boolean, +    step: :integer, +    to: :integer, +    start: :boolean, +    quiet: :boolean, +    log_sql: :boolean, +    migrations_path: :string +  ] + +  @moduledoc """ +  Changes `Logger` level to `:info` before start rollback. +  Changes level back when rollback ends. + +  ## Start rollback + +      mix pleroma.ecto.rollback + +  Options: +    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Rollback.html +  """ + +  @impl true +  def run(args \\ []) do +    load_pleroma() +    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + +    opts = +      if opts[:to] || opts[:step] || opts[:all], +        do: opts, +        else: Keyword.put(opts, :step, 1) + +    opts = +      if opts[:quiet], +        do: Keyword.merge(opts, log: false, log_sql: false), +        else: opts + +    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts) + +    level = Logger.level() +    Logger.configure(level: :info) + +    if Pleroma.Config.get(:env) == :test do +      Logger.info("Rollback succesfully") +    else +      {:ok, _, _} = +        Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts)) +    end + +    Logger.configure(level: level) +  end +end diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index d2ddf450a..c2225af7d 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -55,15 +55,13 @@ defmodule Mix.Tasks.Pleroma.Emoji do    are extracted).    """ -  @default_manifest Pleroma.Config.get!([:emoji, :default_manifest]) -    def run(["ls-packs" | args]) do      Application.ensure_all_started(:hackney)      {options, [], []} = parse_global_opts(args)      manifest = -      fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest) +      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())      Enum.each(manifest, fn {name, info} ->        to_print = [ @@ -88,7 +86,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do      {options, pack_names, []} = parse_global_opts(args) -    manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest +    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()      manifest = fetch_manifest(manifest_url) @@ -298,4 +296,6 @@ defmodule Mix.Tasks.Pleroma.Emoji do      Tesla.client(middleware)    end + +  defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])  end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index d276df93a..0231b76cd 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Instance do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    @shortdoc "Manages Pleroma instance"    @moduledoc """ @@ -29,7 +29,11 @@ defmodule Mix.Tasks.Pleroma.Instance do    - `--dbname DBNAME` - the name of the database to use    - `--dbuser DBUSER` - the user (aka role) to use for the database connection    - `--dbpass DBPASS` - the password to use for the database connection +  - `--rum Y/N` - Whether to enable RUM indexes    - `--indexable Y/N` - Allow/disallow indexing site by search engines +  - `--db-configurable Y/N` - Allow/disallow configuring instance from admin part +  - `--uploads-dir` - the directory uploads go in when using a local uploader +  - `--static-dir` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)    """    def run(["gen" | rest]) do @@ -48,7 +52,11 @@ defmodule Mix.Tasks.Pleroma.Instance do            dbname: :string,            dbuser: :string,            dbpass: :string, -          indexable: :string +          rum: :string, +          indexable: :string, +          db_configurable: :string, +          uploads_dir: :string, +          static_dir: :string          ],          aliases: [            o: :output, @@ -68,7 +76,7 @@ defmodule Mix.Tasks.Pleroma.Instance do      if proceed? do        [domain, port | _] =          String.split( -          Common.get_option( +          get_option(              options,              :domain,              "What domain will your instance use? (e.g pleroma.soykaf.com)" @@ -77,16 +85,16 @@ defmodule Mix.Tasks.Pleroma.Instance do          ) ++ [443]        name = -        Common.get_option( +        get_option(            options,            :instance_name,            "What is the name of your instance? (e.g. Pleroma/Soykaf)"          ) -      email = Common.get_option(options, :admin_email, "What is your admin email address?") +      email = get_option(options, :admin_email, "What is your admin email address?")        notify_email = -        Common.get_option( +        get_option(            options,            :notify_email,            "What email address do you want to use for sending email notifications?", @@ -94,21 +102,27 @@ defmodule Mix.Tasks.Pleroma.Instance do          )        indexable = -        Common.get_option( +        get_option(            options,            :indexable,            "Do you want search engines to index your site? (y/n)",            "y"          ) === "y" -      dbhost = -        Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") +      db_configurable? = +        get_option( +          options, +          :db_configurable, +          "Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)", +          "n" +        ) === "y" -      dbname = -        Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev") +      dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost") + +      dbname = get_option(options, :dbname, "What is the name of your database?", "pleroma")        dbuser = -        Common.get_option( +        get_option(            options,            :dbuser,            "What is the user used to connect to your database?", @@ -116,7 +130,7 @@ defmodule Mix.Tasks.Pleroma.Instance do          )        dbpass = -        Common.get_option( +        get_option(            options,            :dbpass,            "What is the password used to connect to your database?", @@ -124,14 +138,39 @@ defmodule Mix.Tasks.Pleroma.Instance do            "autogenerated"          ) +      rum_enabled = +        get_option( +          options, +          :rum, +          "Would you like to use RUM indices?", +          "n" +        ) === "y" + +      uploads_dir = +        get_option( +          options, +          :upload_dir, +          "What directory should media uploads go in (when using the local uploader)?", +          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) +        ) + +      static_dir = +        get_option( +          options, +          :static_dir, +          "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", +          Pleroma.Config.get([:instance, :static_dir]) +        ) +        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)        signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)        {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1) +      template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"        result_config =          EEx.eval_file( -          "sample_config.eex" |> Path.expand(__DIR__), +          template_dir <> "/sample_config.eex",            domain: domain,            port: port,            email: email, @@ -141,47 +180,39 @@ defmodule Mix.Tasks.Pleroma.Instance do            dbname: dbname,            dbuser: dbuser,            dbpass: dbpass, -          version: Pleroma.Mixfile.project() |> Keyword.get(:version),            secret: secret,            jwt_secret: jwt_secret,            signing_salt: signing_salt,            web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), -          web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) +          web_push_private_key: Base.url_encode64(web_push_private_key, padding: false), +          db_configurable?: db_configurable?, +          static_dir: static_dir, +          uploads_dir: uploads_dir, +          rum_enabled: rum_enabled          )        result_psql =          EEx.eval_file( -          "sample_psql.eex" |> Path.expand(__DIR__), +          template_dir <> "/sample_psql.eex",            dbname: dbname,            dbuser: dbuser, -          dbpass: dbpass +          dbpass: dbpass, +          rum_enabled: rum_enabled          ) -      Mix.shell().info( -        "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs." -      ) +      shell_info("Writing config to #{config_path}.")        File.write(config_path, result_config) -      Mix.shell().info("Writing #{psql_path}.") +      shell_info("Writing the postgres script to #{psql_path}.")        File.write(psql_path, result_psql) -      write_robots_txt(indexable) - -      Mix.shell().info( -        "\n" <> -          """ -          To get started: -          1. Verify the contents of the generated files. -          2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)}`. -          """ <> -          if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do -            "" -          else -            "3. Run `mv #{Common.escape_sh_path(config_path)} 'config/prod.secret.exs'`." -          end +      write_robots_txt(indexable, template_dir) + +      shell_info( +        "\n All files successfully written! Refer to the installation instructions for your platform for next steps"        )      else -      Mix.shell().error( +      shell_error(          "The task would have overwritten the following files:\n" <>            (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>            "Rerun with `--force` to overwrite them." @@ -189,10 +220,10 @@ defmodule Mix.Tasks.Pleroma.Instance do      end    end -  defp write_robots_txt(indexable) do +  defp write_robots_txt(indexable, template_dir) do      robots_txt =        EEx.eval_file( -        Path.expand("robots_txt.eex", __DIR__), +        template_dir <> "/robots_txt.eex",          indexable: indexable        ) @@ -206,10 +237,10 @@ defmodule Mix.Tasks.Pleroma.Instance do      if File.exists?(robots_txt_path) do        File.cp!(robots_txt_path, "#{robots_txt_path}.bak") -      Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") +      shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")      end      File.write(robots_txt_path, robots_txt) -    Mix.shell().info("Writing #{robots_txt_path}.") +    shell_info("Writing #{robots_txt_path}.")    end  end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index fbec473c5..83ed0ed02 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Relay do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.Web.ActivityPub.Relay    @shortdoc "Manages remote relays" @@ -24,24 +24,24 @@ defmodule Mix.Tasks.Pleroma.Relay do    Example: ``mix pleroma.relay unfollow https://example.org/relay``    """    def run(["follow", target]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, _activity} <- Relay.follow(target) do        # put this task to sleep to allow the genserver to push out the messages        :timer.sleep(500)      else -      {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") +      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")      end    end    def run(["unfollow", target]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, _activity} <- Relay.unfollow(target) do        # put this task to sleep to allow the genserver to push out the messages        :timer.sleep(500)      else -      {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") +      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")      end    end  end diff --git a/lib/mix/tasks/pleroma/robots_txt.eex b/lib/mix/tasks/pleroma/robots_txt.eex deleted file mode 100644 index 1af3c47ee..000000000 --- a/lib/mix/tasks/pleroma/robots_txt.eex +++ /dev/null @@ -1,2 +0,0 @@ -User-Agent: * -Disallow: <%= if indexable, do: "", else: "/" %> diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex deleted file mode 100644 index ec7d8821e..000000000 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma instance configuration - -# NOTE: This file should not be committed to a repo or otherwise made public -# without removing sensitive information. - -use Mix.Config - -config :pleroma, Pleroma.Web.Endpoint, -   url: [host: "<%= domain %>", scheme: "https", port: <%= port %>], -   secret_key_base: "<%= secret %>", -   signing_salt: "<%= signing_salt %>" - -config :pleroma, :instance, -  name: "<%= name %>", -  email: "<%= email %>", -  notify_email: "<%= notify_email %>", -  limit: 5000, -  registrations_open: true, -  dedupe_media: false - -config :pleroma, :media_proxy, -  enabled: false, -  redirect_on_failure: true -  #base_url: "https://cache.pleroma.social" - -config :pleroma, Pleroma.Repo, -  adapter: Ecto.Adapters.Postgres, -  username: "<%= dbuser %>", -  password: "<%= dbpass %>", -  database: "<%= dbname %>", -  hostname: "<%= dbhost %>", -  pool_size: 10 - -# Configure web push notifications -config :web_push_encryption, :vapid_details, -  subject: "mailto:<%= email %>", -  public_key: "<%= web_push_public_key %>", -  private_key: "<%= web_push_private_key %>" - -# Enable Strict-Transport-Security once SSL is working: -# config :pleroma, :http_security, -#   sts: true - -# Configure S3 support if desired. -# The public S3 endpoint is different depending on region and provider, -# consult your S3 provider's documentation for details on what to use. -# -# config :pleroma, Pleroma.Uploaders.S3, -#   bucket: "some-bucket", -#   public_endpoint: "https://s3.amazonaws.com" -# -# Configure S3 credentials: -# config :ex_aws, :s3, -#   access_key_id: "xxxxxxxxxxxxx", -#   secret_access_key: "yyyyyyyyyyyy", -#   region: "us-east-1", -#   scheme: "https://" -# -# For using third-party S3 clones like wasabi, also do: -# config :ex_aws, :s3, -#   host: "s3.wasabisys.com" - - -# Configure Openstack Swift support if desired. -# -# Many openstack deployments are different, so config is left very open with -# no assumptions made on which provider you're using. This should allow very -# wide support without needing separate handlers for OVH, Rackspace, etc. -# -# config :pleroma, Pleroma.Uploaders.Swift, -#  container: "some-container", -#  username: "api-username-yyyy", -#  password: "api-key-xxxx", -#  tenant_id: "<openstack-project/tenant-id>", -#  auth_url: "https://keystone-endpoint.provider.com", -#  storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>", -#  object_url: "https://cdn-endpoint.provider.com/<container>" -# - -config :joken, default_signer: "<%= jwt_secret %>" diff --git a/lib/mix/tasks/pleroma/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex deleted file mode 100644 index f0ac05e57..000000000 --- a/lib/mix/tasks/pleroma/sample_psql.eex +++ /dev/null @@ -1,7 +0,0 @@ -CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>'; -CREATE DATABASE <%= dbname %> OWNER <%= dbuser %>; -\c <%= dbname %>; ---Extensions made by ecto.migrate that need superuser access -CREATE EXTENSION IF NOT EXISTS citext; -CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index 106fcf443..be45383ee 100644 --- a/lib/mix/tasks/pleroma/uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -4,7 +4,7 @@  defmodule Mix.Tasks.Pleroma.Uploads do    use Mix.Task -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.Upload    alias Pleroma.Uploaders.Local    require Logger @@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do    """    def run(["migrate_local", target_uploader | args]) do      delete? = Enum.member?(args, "--delete") -    Common.start_pleroma() +    start_pleroma()      local_path = Pleroma.Config.get!([Local, :uploads])      uploader = Module.concat(Pleroma.Uploaders, target_uploader) @@ -38,10 +38,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do        Pleroma.Config.put([Upload, :uploader], uploader)      end -    Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}") +    shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}")      if delete? do -      Mix.shell().info( +      shell_info(          "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"        ) @@ -78,7 +78,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do        |> Enum.filter(& &1)      total_count = length(uploads) -    Mix.shell().info("Found #{total_count} uploads") +    shell_info("Found #{total_count} uploads")      uploads      |> Task.async_stream( @@ -90,7 +90,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do              :ok            error -> -            Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") +            shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")          end        end,        timeout: 150_000 @@ -99,10 +99,10 @@ defmodule Mix.Tasks.Pleroma.Uploads do      # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation      |> Enum.reduce(0, fn done, count ->        count = count + length(done) -      Mix.shell().info("Uploaded #{count}/#{total_count} files") +      shell_info("Uploaded #{count}/#{total_count} files")        count      end) -    Mix.shell().info("Done!") +    shell_info("Done!")    end  end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 25fc40ea7..8a78b4fe6 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -5,9 +5,10 @@  defmodule Mix.Tasks.Pleroma.User do    use Mix.Task    import Ecto.Changeset -  alias Mix.Tasks.Pleroma.Common +  import Mix.Pleroma    alias Pleroma.User    alias Pleroma.UserInviteToken +  alias Pleroma.Web.OAuth    @shortdoc "Manages Pleroma users"    @moduledoc """ @@ -49,6 +50,10 @@ defmodule Mix.Tasks.Pleroma.User do        mix pleroma.user delete_activities NICKNAME +  ## Sign user out from all applications (delete user's OAuth tokens and authorizations). + +      mix pleroma.user sign_out NICKNAME +    ## Deactivate or activate the user's account.        mix pleroma.user toggle_activated NICKNAME @@ -115,7 +120,7 @@ defmodule Mix.Tasks.Pleroma.User do      admin? = Keyword.get(options, :admin, false)      assume_yes? = Keyword.get(options, :assume_yes, false) -    Mix.shell().info(""" +    shell_info("""      A user will be created with the following information:        - nickname: #{nickname}        - email: #{email} @@ -128,10 +133,10 @@ defmodule Mix.Tasks.Pleroma.User do        - admin: #{if(admin?, do: "true", else: "false")}      """) -    proceed? = assume_yes? or Mix.shell().yes?("Continue?") +    proceed? = assume_yes? or shell_yes?("Continue?")      if proceed? do -      Common.start_pleroma() +      start_pleroma()        params = %{          nickname: nickname, @@ -145,7 +150,7 @@ defmodule Mix.Tasks.Pleroma.User do        changeset = User.register_changeset(%User{}, params, need_confirmation: false)        {:ok, _user} = User.register(changeset) -      Mix.shell().info("User #{nickname} created") +      shell_info("User #{nickname} created")        if moderator? do          run(["set", nickname, "--moderator"]) @@ -159,64 +164,64 @@ defmodule Mix.Tasks.Pleroma.User do          run(["reset_password", nickname])        end      else -      Mix.shell().info("User will not be created.") +      shell_info("User will not be created.")      end    end    def run(["rm", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        User.perform(:delete, user) -      Mix.shell().info("User #{nickname} deleted.") +      shell_info("User #{nickname} deleted.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["toggle_activated", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        {:ok, user} = User.deactivate(user, !user.info.deactivated) -      Mix.shell().info( +      shell_info(          "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"        )      else        _ -> -        Mix.shell().error("No user #{nickname}") +        shell_error("No user #{nickname}")      end    end    def run(["reset_password", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname),           {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do -      Mix.shell().info("Generated password reset token for #{user.nickname}") +      shell_info("Generated password reset token for #{user.nickname}")        IO.puts(          "URL: #{ -          Pleroma.Web.Router.Helpers.util_url( +          Pleroma.Web.Router.Helpers.reset_password_url(              Pleroma.Web.Endpoint, -            :show_password_reset, +            :reset,              token.token            )          }"        )      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["unsubscribe", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do -      Mix.shell().info("Deactivating #{user.nickname}") +      shell_info("Deactivating #{user.nickname}")        User.deactivate(user)        {:ok, friends} = User.get_friends(user) @@ -224,7 +229,7 @@ defmodule Mix.Tasks.Pleroma.User do        Enum.each(friends, fn friend ->          user = User.get_cached_by_id(user.id) -        Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}") +        shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")          User.unfollow(user, friend)        end) @@ -233,16 +238,16 @@ defmodule Mix.Tasks.Pleroma.User do        user = User.get_cached_by_id(user.id)        if Enum.empty?(user.following) do -        Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") +        shell_info("Successfully unsubscribed all followers from #{user.nickname}")        end      else        _ -> -        Mix.shell().error("No user #{nickname}") +        shell_error("No user #{nickname}")      end    end    def run(["set", nickname | rest]) do -    Common.start_pleroma() +    start_pleroma()      {options, [], []} =        OptionParser.parse( @@ -274,33 +279,33 @@ defmodule Mix.Tasks.Pleroma.User do          end      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["tag", nickname | tags]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.tag(tags) -      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")      else        _ -> -        Mix.shell().error("Could not change user tags for #{nickname}") +        shell_error("Could not change user tags for #{nickname}")      end    end    def run(["untag", nickname | tags]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        user = user |> User.untag(tags) -      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") +      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")      else        _ -> -        Mix.shell().error("Could not change user tags for #{nickname}") +        shell_error("Could not change user tags for #{nickname}")      end    end @@ -321,14 +326,12 @@ defmodule Mix.Tasks.Pleroma.User do        end)        |> Enum.into(%{}) -    Common.start_pleroma() +    start_pleroma()      with {:ok, val} <- options[:expires_at],           options = Map.put(options, :expires_at, val),           {:ok, invite} <- UserInviteToken.create_invite(options) do -      Mix.shell().info( -        "Generated user invite token " <> String.replace(invite.invite_type, "_", " ") -      ) +      shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))        url =          Pleroma.Web.Router.Helpers.redirect_url( @@ -340,14 +343,14 @@ defmodule Mix.Tasks.Pleroma.User do        IO.puts(url)      else        error -> -        Mix.shell().error("Could not create invite token: #{inspect(error)}") +        shell_error("Could not create invite token: #{inspect(error)}")      end    end    def run(["invites"]) do -    Common.start_pleroma() +    start_pleroma() -    Mix.shell().info("Invites list:") +    shell_info("Invites list:")      UserInviteToken.list_invites()      |> Enum.each(fn invite -> @@ -361,7 +364,7 @@ defmodule Mix.Tasks.Pleroma.User do            " | Max use: #{max_use}    Left use: #{max_use - invite.uses}"          end -      Mix.shell().info( +      shell_info(          "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{            invite.used          }#{expire_info}#{using_info}" @@ -370,40 +373,54 @@ defmodule Mix.Tasks.Pleroma.User do    end    def run(["revoke_invite", token]) do -    Common.start_pleroma() +    start_pleroma()      with {:ok, invite} <- UserInviteToken.find_by_token(token),           {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do -      Mix.shell().info("Invite for token #{token} was revoked.") +      shell_info("Invite for token #{token} was revoked.")      else -      _ -> Mix.shell().error("No invite found with token #{token}") +      _ -> shell_error("No invite found with token #{token}")      end    end    def run(["delete_activities", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do        {:ok, _} = User.delete_user_activities(user) -      Mix.shell().info("User #{nickname} statuses deleted.") +      shell_info("User #{nickname} statuses deleted.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end    def run(["toggle_confirmed", nickname]) do -    Common.start_pleroma() +    start_pleroma()      with %User{} = user <- User.get_cached_by_nickname(nickname) do        {:ok, user} = User.toggle_confirmation(user)        message = if user.info.confirmation_pending, do: "needs", else: "doesn't need" -      Mix.shell().info("#{nickname} #{message} confirmation.") +      shell_info("#{nickname} #{message} confirmation.") +    else +      _ -> +        shell_error("No local user #{nickname}") +    end +  end + +  def run(["sign_out", nickname]) do +    start_pleroma() + +    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do +      OAuth.Token.delete_user_tokens(user) +      OAuth.Authorization.delete_user_authorizations(user) + +      shell_info("#{nickname} signed out from all apps.")      else        _ -> -        Mix.shell().error("No local user #{nickname}") +        shell_error("No local user #{nickname}")      end    end @@ -416,7 +433,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") +    shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")      user    end @@ -429,7 +446,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}") +    shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")      user    end @@ -442,7 +459,7 @@ defmodule Mix.Tasks.Pleroma.User do      {:ok, user} = User.update_and_set_cache(user_cng) -    Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}") +    shell_info("Locked status of #{user.nickname}: #{user.info.locked}")      user    end  end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 9ccedcd13..0aa2aab23 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -39,8 +39,7 @@ defmodule Pleroma.Activity.Search do            "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",            o.data,            ^search_query -        ), -      order_by: [desc: :id] +        )      )    end @@ -56,18 +55,19 @@ defmodule Pleroma.Activity.Search do      )    end -  # users can search everything -  defp maybe_restrict_local(q, %User{}), do: q +  defp maybe_restrict_local(q, user) do +    limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) -  # unauthenticated users can only search local activities -  defp maybe_restrict_local(q, _) do -    if Pleroma.Config.get([:instance, :limit_unauthenticated_to_local_content], true) do -      where(q, local: true) -    else -      q +    case {limit, user} do +      {:all, _} -> restrict_local(q) +      {:unauthenticated, %User{}} -> q +      {:unauthenticated, _} -> restrict_local(q) +      {false, _} -> q      end    end +  defp restrict_local(q), do: where(q, local: true) +    defp maybe_fetch(activities, user, search_query) do      with true <- Regex.match?(~r/https?:/, search_query),           {:ok, object} <- Fetcher.fetch_object_from_id(search_query), diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 664cf578e..29cd14477 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -31,6 +31,7 @@ defmodule Pleroma.Application do        [          # Start the Ecto repository          %{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor}, +        %{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}},          %{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}},          %{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}},          %{ @@ -180,7 +181,6 @@ defmodule Pleroma.Application do        Pleroma.Repo.Instrumenter.setup()      end -    Prometheus.Registry.register_collector(:prometheus_process_collector)      Pleroma.Web.Endpoint.MetricsExporter.setup()      Pleroma.Web.Endpoint.PipelineInstrumenter.setup()      Pleroma.Web.Endpoint.Instrumenter.setup() @@ -193,14 +193,14 @@ defmodule Pleroma.Application do        else          []        end ++ -      if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do +      if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do          [:upload]        else          []        end    end -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      defp streamer_child, do: []      defp chat_child, do: []    else diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex new file mode 100644 index 000000000..cf880aa22 --- /dev/null +++ b/lib/pleroma/config/transfer_task.ex @@ -0,0 +1,55 @@ +defmodule Pleroma.Config.TransferTask do +  use Task +  alias Pleroma.Web.AdminAPI.Config + +  def start_link do +    load_and_update_env() +    if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo) +    :ignore +  end + +  def load_and_update_env do +    if Pleroma.Config.get([:instance, :dynamic_configuration]) and +         Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do +      for_restart = +        Pleroma.Repo.all(Config) +        |> Enum.map(&update_env(&1)) + +      # We need to restart applications for loaded settings take effect +      for_restart +      |> Enum.reject(&(&1 in [:pleroma, :ok])) +      |> Enum.each(fn app -> +        Application.stop(app) +        :ok = Application.start(app) +      end) +    end +  end + +  defp update_env(setting) do +    try do +      key = +        if String.starts_with?(setting.key, "Pleroma.") do +          "Elixir." <> setting.key +        else +          setting.key +        end + +      group = String.to_existing_atom(setting.group) + +      Application.put_env( +        group, +        String.to_existing_atom(key), +        Config.from_binary(setting.value) +      ) + +      group +    rescue +      e -> +        require Logger + +        Logger.warn( +          "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}" +        ) +    end +  end +end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 2c13c4b40..5883e4183 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -59,10 +59,10 @@ defmodule Pleroma.Conversation.Participation do    def for_user(user, params \\ %{}) do      from(p in __MODULE__,        where: p.user_id == ^user.id, -      order_by: [desc: p.updated_at] +      order_by: [desc: p.updated_at], +      preload: [conversation: [:users]]      )      |> Pleroma.Pagination.fetch_paginated(params) -    |> Repo.preload(conversation: [:users])    end    def for_user_with_last_activity_id(user, params \\ %{}) do diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 0ad0aed40..49046bb8b 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -23,13 +23,8 @@ defmodule Pleroma.Emails.UserEmail do    defp recipient(email, name), do: {name, email}    defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name) -  def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do -    password_reset_url = -      Router.Helpers.util_url( -        Endpoint, -        :show_password_reset, -        password_reset_token -      ) +  def password_reset_email(user, token) when is_binary(token) do +    password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)      html_body = """      <h3>Reset your password at #{instance_name()}</h3> diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index de7fcc1ce..052501642 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do    @ets __MODULE__.Ets    @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] -  @groups Pleroma.Config.get([:emoji, :groups])    @doc false    def start_link do @@ -87,6 +86,8 @@ defmodule Pleroma.Emoji do          "emoji"        ) +    emoji_groups = Pleroma.Config.get([:emoji, :groups]) +      case File.ls(emoji_dir_path) do        {:error, :enoent} ->          # The custom emoji directory doesn't exist, @@ -98,7 +99,9 @@ defmodule Pleroma.Emoji do          Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")        {:ok, results} -> -        grouped = Enum.group_by(results, &File.dir?/1) +        grouped = +          Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end) +          packs = grouped[true] || []          files = grouped[false] || [] @@ -116,7 +119,7 @@ defmodule Pleroma.Emoji do          emojis =            Enum.flat_map(              packs, -            fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end +            fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end            )          true = :ets.insert(@ets, emojis) @@ -127,9 +130,9 @@ defmodule Pleroma.Emoji do      shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])      emojis = -      (load_from_file("config/emoji.txt") ++ -         load_from_file("config/custom_emoji.txt") ++ -         load_from_globs(shortcode_globs)) +      (load_from_file("config/emoji.txt", emoji_groups) ++ +         load_from_file("config/custom_emoji.txt", emoji_groups) ++ +         load_from_globs(shortcode_globs, emoji_groups))        |> Enum.reject(fn value -> value == nil end)      true = :ets.insert(@ets, emojis) @@ -137,23 +140,25 @@ defmodule Pleroma.Emoji do      :ok    end -  defp load_pack(pack_dir) do +  defp load_pack(pack_dir, emoji_groups) do      pack_name = Path.basename(pack_dir)      emoji_txt = Path.join(pack_dir, "emoji.txt")      if File.exists?(emoji_txt) do -      load_from_file(emoji_txt) +      load_from_file(emoji_txt, emoji_groups)      else +      extensions = Pleroma.Config.get([:emoji, :pack_extensions]) +        Logger.info( -        "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji" +        "No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"        ) -      make_shortcode_to_file_map(pack_dir, [".png"]) +      make_shortcode_to_file_map(pack_dir, extensions)        |> Enum.map(fn {shortcode, rel_file} ->          filename = Path.join("/emoji/#{pack_name}", rel_file) -        {shortcode, filename, [to_string(match_extra(@groups, filename))]} +        {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}        end)      end    end @@ -182,21 +187,21 @@ defmodule Pleroma.Emoji do      |> Enum.filter(fn f -> Path.extname(f) in exts end)    end -  defp load_from_file(file) do +  defp load_from_file(file, emoji_groups) do      if File.exists?(file) do -      load_from_file_stream(File.stream!(file)) +      load_from_file_stream(File.stream!(file), emoji_groups)      else        []      end    end -  defp load_from_file_stream(stream) do +  defp load_from_file_stream(stream, emoji_groups) do      stream      |> Stream.map(&String.trim/1)      |> Stream.map(fn line ->        case String.split(line, ~r/,\s*/) do          [name, file] -> -          {name, file, [to_string(match_extra(@groups, file))]} +          {name, file, [to_string(match_extra(emoji_groups, file))]}          [name, file | tags] ->            {name, file, tags} @@ -208,7 +213,7 @@ defmodule Pleroma.Emoji do      |> Enum.to_list()    end -  defp load_from_globs(globs) do +  defp load_from_globs(globs, emoji_groups) do      static_path = Path.join(:code.priv_dir(:pleroma), "static")      paths = @@ -219,7 +224,7 @@ defmodule Pleroma.Emoji do        |> Enum.concat()      Enum.map(paths, fn path -> -      tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path))) +      tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))        shortcode = Path.basename(path, Path.extname(path))        external_path = Path.join("/", Path.relative_to(path, static_path))        {shortcode, external_path, [to_string(tag)]} diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex new file mode 100644 index 000000000..8a79b44c4 --- /dev/null +++ b/lib/pleroma/helpers/uri_helper.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.UriHelper do +  def append_uri_params(uri, appended_params) do +    uri = URI.parse(uri) +    appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v} +    existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{}) +    updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params)) + +    updated_params = +      for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]} + +    uri +    |> Map.put(:query, URI.encode_query(updated_params)) +    |> URI.to_string() +  end + +  def append_param_if_present(%{} = params, param_name, param_value) do +    if param_value do +      Map.put(params, param_name, param_value) +    else +      params +    end +  end +end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index e5e78ee4f..2fae7281c 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -89,7 +89,7 @@ defmodule Pleroma.HTML do      Cachex.fetch!(:scrubber_cache, key, fn _key ->        result =          content -        |> Floki.filter_out("a.mention") +        |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")          |> Floki.attribute("a", "href")          |> Enum.at(0) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 5e107f4c9..fa5043bc5 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Instances do    def reachability_datetime_threshold do      federation_reachability_timeout_days = -      Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0 +      Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0)      if federation_reachability_timeout_days > 0 do        NaiveDateTime.add( diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 736ebc3af..118560d34 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -13,6 +13,8 @@ defmodule Pleroma.Notification do    alias Pleroma.User    alias Pleroma.Web.CommonAPI    alias Pleroma.Web.CommonAPI.Utils +  alias Pleroma.Web.Push +  alias Pleroma.Web.Streamer    import Ecto.Query    import Ecto.Changeset @@ -149,8 +151,7 @@ defmodule Pleroma.Notification do      end    end -  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) -      when type in ["Create", "Like", "Announce", "Follow"] do +  def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do      object = Object.normalize(activity)      unless object && object.data["type"] == "Answer" do @@ -162,6 +163,13 @@ defmodule Pleroma.Notification do      end    end +  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) +      when type in ["Like", "Announce", "Follow"] do +    users = get_notified_from_activity(activity) +    notifications = Enum.map(users, fn user -> create_notification(activity, user) end) +    {:ok, notifications} +  end +    def create_notifications(_), do: {:ok, []}    # TODO move to sql, too. @@ -169,8 +177,9 @@ defmodule Pleroma.Notification do      unless skip?(activity, user) do        notification = %Notification{user_id: user.id, activity: activity}        {:ok, notification} = Repo.insert(notification) -      Pleroma.Web.Streamer.stream("user", notification) -      Pleroma.Web.Push.send(notification) +      Streamer.stream("user", notification) +      Streamer.stream("user:notification", notification) +      Push.send(notification)        notification      end    end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 2f4687fa2..ada9da0bb 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +  defmodule Pleroma.Object.Containment do    @moduledoc """    This module contains some useful functions for containing objects to specific diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index ca980c629..c422490ac 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -85,6 +85,9 @@ defmodule Pleroma.Object.Fetcher do           :ok <- Containment.contain_origin_from_id(id, data) do        {:ok, data}      else +      {:ok, %{status: code}} when code in [404, 410] -> +        {:error, "Object has been deleted"} +        e ->          {:error, e}      end diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/password_reset_token.ex index f31ea5bc5..4a833f6a5 100644 --- a/lib/pleroma/PasswordResetToken.ex +++ b/lib/pleroma/password_reset_token.ex @@ -37,6 +37,7 @@ defmodule Pleroma.PasswordResetToken do      |> put_change(:used, true)    end +  @spec reset_password(binary(), map()) :: {:ok, User.t()} | {:error, binary()}    def reset_password(token, data) do      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),           %User{} = user <- User.get_cached_by_id(token.user_id), diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 485ddfbc7..a7cc22831 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -56,14 +56,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do      connect_src = "connect-src 'self' #{static_url} #{websocket_url}"      connect_src = -      if Mix.env() == :dev do +      if Pleroma.Config.get(:env) == :dev do          connect_src <> " http://localhost:3035/"        else          connect_src        end      script_src = -      if Mix.env() == :dev do +      if Pleroma.Config.get(:env) == :dev do          "script-src 'self' 'unsafe-eval'"        else          "script-src 'self'" diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex new file mode 100644 index 000000000..e99c5d279 --- /dev/null +++ b/lib/pleroma/plugs/idempotency_plug.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.IdempotencyPlug do +  import Phoenix.Controller, only: [json: 2] +  import Plug.Conn + +  @behaviour Plug + +  @impl true +  def init(opts), do: opts + +  # Sending idempotency keys in `GET` and `DELETE` requests has no effect +  # and should be avoided, as these requests are idempotent by definition. + +  @impl true +  def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do +    case get_req_header(conn, "idempotency-key") do +      [key] -> process_request(conn, key) +      _ -> conn +    end +  end + +  def call(conn, _), do: conn + +  def process_request(conn, key) do +    case Cachex.get(:idempotency_cache, key) do +      {:ok, nil} -> +        cache_resposnse(conn, key) + +      {:ok, record} -> +        send_cached(conn, key, record) + +      {atom, message} when atom in [:ignore, :error] -> +        render_error(conn, message) +    end +  end + +  defp cache_resposnse(conn, key) do +    register_before_send(conn, fn conn -> +      [request_id] = get_resp_header(conn, "x-request-id") +      content_type = get_content_type(conn) + +      record = {request_id, content_type, conn.status, conn.resp_body} +      {:ok, _} = Cachex.put(:idempotency_cache, key, record) + +      conn +      |> put_resp_header("idempotency-key", key) +      |> put_resp_header("x-original-request-id", request_id) +    end) +  end + +  defp send_cached(conn, key, record) do +    {request_id, content_type, status, body} = record + +    conn +    |> put_resp_header("idempotency-key", key) +    |> put_resp_header("idempotent-replayed", "true") +    |> put_resp_header("x-original-request-id", request_id) +    |> put_resp_content_type(content_type) +    |> send_resp(status, body) +    |> halt() +  end + +  defp render_error(conn, message) do +    conn +    |> put_status(:unprocessable_entity) +    |> json(%{error: message}) +    |> halt() +  end + +  defp get_content_type(conn) do +    [content_type] = get_resp_header(conn, "content-type") + +    if String.contains?(content_type, ";") do +      content_type +      |> String.split(";") +      |> hd() +    else +      content_type +    end +  end +end diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex deleted file mode 100644 index 466f64a79..000000000 --- a/lib/pleroma/plugs/rate_limit_plug.ex +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimitPlug do -  import Phoenix.Controller, only: [json: 2] -  import Plug.Conn - -  def init(opts), do: opts - -  def call(conn, opts) do -    enabled? = Pleroma.Config.get([:app_account_creation, :enabled]) - -    case check_rate(conn, Map.put(opts, :enabled, enabled?)) do -      {:ok, _count} -> conn -      {:error, _count} -> render_error(conn) -      %Plug.Conn{} = conn -> conn -    end -  end - -  defp check_rate(conn, %{enabled: true} = opts) do -    max_requests = opts[:max_requests] -    bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") - -    ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests) -  end - -  defp check_rate(conn, _), do: conn - -  defp render_error(conn) do -    conn -    |> put_status(:forbidden) -    |> json(%{error: "Rate limit exceeded."}) -    |> halt() -  end -end diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex new file mode 100644 index 000000000..9ba5875fa --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -0,0 +1,94 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter do +  @moduledoc """ + +  ## Configuration + +  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: + +  * The first element: `scale` (Integer). The time scale in milliseconds. +  * The second element: `limit` (Integer). How many requests to limit in the time scale provided. + +  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + +  To disable a limiter set its value to `nil`. + +  ### Example + +      config :pleroma, :rate_limit, +        one: {1000, 10}, +        two: [{10_000, 10}, {10_000, 50}], +        foobar: nil + +  Here we have three limiters: + +  * `one` which is not over 10req/1s +  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users +  * `foobar` which is disabled + +  ## Usage + +  Inside a controller: + +      plug(Pleroma.Plugs.RateLimiter, :one when action == :one) +      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) + +  or inside a router pipiline: + +      pipeline :api do +        ... +        plug(Pleroma.Plugs.RateLimiter, :one) +        ... +      end +  """ + +  import Phoenix.Controller, only: [json: 2] +  import Plug.Conn + +  alias Pleroma.User + +  def init(limiter_name) do +    case Pleroma.Config.get([:rate_limit, limiter_name]) do +      nil -> nil +      config -> {limiter_name, config} +    end +  end + +  # do not limit if there is no limiter configuration +  def call(conn, nil), do: conn + +  def call(conn, opts) do +    case check_rate(conn, opts) do +      {:ok, _count} -> conn +      {:error, _count} -> render_error(conn) +    end +  end + +  defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do +    ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit) +  end + +  defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do +    ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit) +  end + +  defp check_rate(conn, {limiter_name, {scale, limit}}) do +    check_rate(conn, {limiter_name, [{scale, limit}]}) +  end + +  def ip(%{remote_ip: remote_ip}) do +    remote_ip +    |> Tuple.to_list() +    |> Enum.join(".") +  end + +  defp render_error(conn) do +    conn +    |> put_status(:too_many_requests) +    |> json(%{error: "Throttled"}) +    |> halt() +  end +end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index fd77b8d8f..8d0fac7ee 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Plugs.UploadedMedia do            conn        end -    config = Pleroma.Config.get([Pleroma.Upload]) +    config = Pleroma.Config.get(Pleroma.Upload)      with uploader <- Keyword.fetch!(config, :uploader),           proxy_remote = Keyword.get(config, :proxy_remote, false), diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex new file mode 100644 index 000000000..8afabf463 --- /dev/null +++ b/lib/pleroma/release_tasks.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReleaseTasks do +  @repo Pleroma.Repo + +  def run(args) do +    [task | args] = String.split(args) + +    case task do +      "migrate" -> migrate(args) +      "create" -> create() +      "rollback" -> rollback(args) +      task -> mix_task(task, args) +    end +  end + +  defp mix_task(task, args) do +    Application.load(:pleroma) +    {:ok, modules} = :application.get_key(:pleroma, :modules) + +    module = +      Enum.find(modules, fn module -> +        module = Module.split(module) + +        match?(["Mix", "Tasks", "Pleroma" | _], module) and +          String.downcase(List.last(module)) == task +      end) + +    if module do +      module.run(args) +    else +      IO.puts("The task #{task} does not exist") +    end +  end + +  def migrate(args) do +    Mix.Tasks.Pleroma.Ecto.Migrate.run(args) +  end + +  def rollback(args) do +    Mix.Tasks.Pleroma.Ecto.Rollback.run(args) +  end + +  def create do +    Application.load(:pleroma) + +    case @repo.__adapter__.storage_up(@repo.config) do +      :ok -> +        IO.puts("The database for #{inspect(@repo)} has been created") + +      {:error, :already_up} -> +        IO.puts("The database for #{inspect(@repo)} has already been created") + +      {:error, term} when is_binary(term) -> +        IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}") + +      {:error, term} -> +        IO.puts( +          :stderr, +          "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}" +        ) +    end +  end +end diff --git a/lib/pleroma/repo_streamer.ex b/lib/pleroma/repo_streamer.ex new file mode 100644 index 000000000..a4b71a1bb --- /dev/null +++ b/lib/pleroma/repo_streamer.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RepoStreamer do +  alias Pleroma.Repo +  import Ecto.Query + +  def chunk_stream(query, chunk_size) do +    Stream.unfold(0, fn +      :halt -> +        {[], :halt} + +      last_id -> +        query +        |> order_by(asc: :id) +        |> where([r], r.id > ^last_id) +        |> limit(^chunk_size) +        |> Repo.all() +        |> case do +          [] -> +            {[], :halt} + +          records -> +            last_id = List.last(records).id +            {records, last_id} +        end +    end) +    |> Stream.take_while(fn +      [] -> false +      _ -> true +    end) +  end +end diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 285d57309..de0f6e1bc 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -146,7 +146,7 @@ defmodule Pleroma.ReverseProxy do      Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")      method = method |> String.downcase() |> String.to_existing_atom() -    case :hackney.request(method, url, headers, "", hackney_opts) do +    case hackney().request(method, url, headers, "", hackney_opts) do        {:ok, code, headers, client} when code in @valid_resp_codes ->          {:ok, code, downcase_headers(headers), client} @@ -196,7 +196,7 @@ defmodule Pleroma.ReverseProxy do               duration,               Keyword.get(opts, :max_read_duration, @max_read_duration)             ), -         {:ok, data} <- :hackney.stream_body(client), +         {:ok, data} <- hackney().stream_body(client),           {:ok, duration} <- increase_read_duration(duration),           sent_so_far = sent_so_far + byte_size(data),           :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), @@ -377,4 +377,6 @@ defmodule Pleroma.ReverseProxy do    defp increase_read_duration(_) do      {:ok, :no_duration_limit, :no_duration_limit}    end + +  defp hackney, do: Pleroma.Config.get(:hackney, :hackney)  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 3993a93e6..9be4b1483 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -9,12 +9,14 @@ defmodule Pleroma.User do    import Ecto.Query    alias Comeonin.Pbkdf2 +  alias Ecto.Multi    alias Pleroma.Activity    alias Pleroma.Keys    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Registration    alias Pleroma.Repo +  alias Pleroma.RepoStreamer    alias Pleroma.User    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub @@ -194,29 +196,26 @@ defmodule Pleroma.User do    end    def password_update_changeset(struct, params) do -    changeset = -      struct -      |> cast(params, [:password, :password_confirmation]) -      |> validate_required([:password, :password_confirmation]) -      |> validate_confirmation(:password) - -    OAuth.Token.delete_user_tokens(struct) -    OAuth.Authorization.delete_user_authorizations(struct) - -    if changeset.valid? do -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) - -      changeset -      |> put_change(:password_hash, hashed) -    else -      changeset +    struct +    |> cast(params, [:password, :password_confirmation]) +    |> validate_required([:password, :password_confirmation]) +    |> validate_confirmation(:password) +    |> put_password_hash +  end + +  def reset_password(%User{id: user_id} = user, data) do +    multi = +      Multi.new() +      |> Multi.update(:user, password_update_changeset(user, data)) +      |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) +      |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) + +    case Repo.transaction(multi) do +      {:ok, %{user: user} = _} -> set_cache(user) +      {:error, _, changeset, _} -> {:error, changeset}      end    end -  def reset_password(user, data) do -    update_and_set_cache(password_update_changeset(user, data)) -  end -    def register_changeset(struct, params \\ %{}, opts \\ []) do      need_confirmation? =        if is_nil(opts[:need_confirmation]) do @@ -250,12 +249,11 @@ defmodule Pleroma.User do        end      if changeset.valid? do -      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})        followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})        changeset -      |> put_change(:password_hash, hashed) +      |> put_password_hash        |> put_change(:ap_id, ap_id)        |> unique_constraint(:ap_id)        |> put_change(:following, [followers]) @@ -933,18 +931,24 @@ defmodule Pleroma.User do    @spec perform(atom(), User.t()) :: {:ok, User.t()}    def perform(:delete, %User{} = user) do -    {:ok, user} = User.deactivate(user) -      # Remove all relationships      {:ok, followers} = User.get_followers(user) -    Enum.each(followers, fn follower -> User.unfollow(follower, user) end) +    Enum.each(followers, fn follower -> +      ActivityPub.unfollow(follower, user) +      User.unfollow(follower, user) +    end)      {:ok, friends} = User.get_friends(user) -    Enum.each(friends, fn followed -> User.unfollow(user, followed) end) +    Enum.each(friends, fn followed -> +      ActivityPub.unfollow(user, followed) +      User.unfollow(user, followed) +    end)      delete_user_activities(user) + +    {:ok, _user} = Repo.delete(user)    end    @spec perform(atom(), User.t()) :: {:ok, User.t()} @@ -1017,18 +1021,35 @@ defmodule Pleroma.User do        ])    def delete_user_activities(%User{ap_id: ap_id} = user) do -    stream = -      ap_id -      |> Activity.query_by_actor() -      |> Repo.stream() - -    Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) +    ap_id +    |> Activity.query_by_actor() +    |> RepoStreamer.chunk_stream(50) +    |> Stream.each(fn activities -> +      Enum.each(activities, &delete_activity(&1)) +    end) +    |> Stream.run()      {:ok, user}    end    defp delete_activity(%{data: %{"type" => "Create"}} = activity) do -    Object.normalize(activity) |> ActivityPub.delete() +    activity +    |> Object.normalize() +    |> ActivityPub.delete() +  end + +  defp delete_activity(%{data: %{"type" => "Like"}} = activity) do +    user = get_cached_by_ap_id(activity.actor) +    object = Object.normalize(activity) + +    ActivityPub.unlike(user, object) +  end + +  defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do +    user = get_cached_by_ap_id(activity.actor) +    object = Object.normalize(activity) + +    ActivityPub.unannounce(user, object)    end    defp delete_activity(_activity), do: "Doing nothing" @@ -1037,9 +1058,7 @@ defmodule Pleroma.User do      Pleroma.HTML.Scrubber.TwitterText    end -  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy]) - -  def html_filter_policy(_), do: @default_scrubbers +  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])    def fetch_by_ap_id(ap_id) do      ap_try = ActivityPub.make_user_from_ap_id(ap_id) @@ -1402,4 +1421,12 @@ defmodule Pleroma.User do    end    defdelegate search(query, opts \\ []), to: User.Search + +  defp put_password_hash( +         %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset +       ) do +    change(changeset, password_hash: Pbkdf2.hashpwsalt(password)) +  end + +  defp put_password_hash(changeset), do: changeset  end diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index add6a0bbf..ed06c2ab9 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -7,74 +7,97 @@ defmodule Pleroma.User.Search do    alias Pleroma.User    import Ecto.Query -  def search(query, opts \\ []) do +  @similarity_threshold 0.25 +  @limit 20 + +  def search(query_string, opts \\ []) do      resolve = Keyword.get(opts, :resolve, false) +    following = Keyword.get(opts, :following, false) +    result_limit = Keyword.get(opts, :limit, @limit) +    offset = Keyword.get(opts, :offset, 0) +      for_user = Keyword.get(opts, :for_user)      # Strip the beginning @ off if there is a query -    query = String.trim_leading(query, "@") +    query_string = String.trim_leading(query_string, "@") -    maybe_resolve(resolve, for_user, query) +    maybe_resolve(resolve, for_user, query_string)      {:ok, results} =        Repo.transaction(fn -> -        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) +        Ecto.Adapters.SQL.query( +          Repo, +          "select set_limit(#{@similarity_threshold})", +          [] +        ) -        query -        |> search_query(for_user) +        query_string +        |> search_query(for_user, following) +        |> paginate(result_limit, offset)          |> Repo.all()        end)      results    end -  defp maybe_resolve(true, %User{}, query) do -    User.get_or_fetch(query) -  end - -  defp maybe_resolve(true, _, query) do -    unless restrict_local?(), do: User.get_or_fetch(query) -  end - -  defp maybe_resolve(_, _, _), do: :noop - -  defp search_query(query, for_user) do -    query -    |> union_query() +  defp search_query(query_string, for_user, following) do +    for_user +    |> base_query(following) +    |> search_subqueries(query_string) +    |> union_subqueries      |> distinct_query()      |> boost_search_rank_query(for_user)      |> subquery()      |> order_by(desc: :search_rank) -    |> limit(20)      |> maybe_restrict_local(for_user)    end -  defp restrict_local? do -    Pleroma.Config.get([:instance, :limit_unauthenticated_to_local_content], true) -  end +  defp base_query(_user, false), do: User +  defp base_query(user, true), do: User.get_followers_query(user) -  defp union_query(query) do -    fts_subquery = fts_search_subquery(query) -    trigram_subquery = trigram_search_subquery(query) +  defp paginate(query, limit, offset) do +    from(q in query, limit: ^limit, offset: ^offset) +  end +  defp union_subqueries({fts_subquery, trigram_subquery}) do      from(s in trigram_subquery, union_all: ^fts_subquery)    end +  defp search_subqueries(base_query, query_string) do +    { +      fts_search_subquery(base_query, query_string), +      trigram_search_subquery(base_query, query_string) +    } +  end +    defp distinct_query(q) do      from(s in subquery(q), order_by: s.search_type, distinct: s.id)    end -  # unauthenticated users can only search local activities -  defp maybe_restrict_local(q, %User{}), do: q +  defp maybe_resolve(true, user, query) do +    case {limit(), user} do +      {:all, _} -> :noop +      {:unauthenticated, %User{}} -> User.get_or_fetch(query) +      {:unauthenticated, _} -> :noop +      {false, _} -> User.get_or_fetch(query) +    end +  end + +  defp maybe_resolve(_, _, _), do: :noop -  defp maybe_restrict_local(q, _) do -    if restrict_local?() do -      where(q, [u], u.local == true) -    else -      q +  defp maybe_restrict_local(q, user) do +    case {limit(), user} do +      {:all, _} -> restrict_local(q) +      {:unauthenticated, %User{}} -> q +      {:unauthenticated, _} -> restrict_local(q) +      {false, _} -> q      end    end +  defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) + +  defp restrict_local(q), do: where(q, [u], u.local == true) +    defp boost_search_rank_query(query, nil), do: query    defp boost_search_rank_query(query, for_user) do @@ -103,7 +126,8 @@ defmodule Pleroma.User.Search do      )    end -  defp fts_search_subquery(term, query \\ User) do +  @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() +  defp fts_search_subquery(query, term) do      processed_query =        term        |> String.replace(~r/\W+/, " ") @@ -145,9 +169,10 @@ defmodule Pleroma.User.Search do      |> User.restrict_deactivated()    end -  defp trigram_search_subquery(term) do +  @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() +  defp trigram_search_subquery(query, term) do      from( -      u in User, +      u in query,        select_merge: %{          # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason          search_type: fragment("?", 1), diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c0e3d1478..55315d66e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -189,6 +189,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end)    end +  def stream_out_participations(%Object{data: %{"context" => context}}, user) do +    with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), +         conversation = Repo.preload(conversation, :participations), +         last_activity_id = +           fetch_latest_activity_id_for_context(conversation.ap_id, %{ +             "user" => user, +             "blocking_user" => user +           }) do +      if last_activity_id do +        stream_out_participations(conversation.participations) +      end +    end +  end + +  def stream_out_participations(_, _), do: :noop +    def stream_out(activity) do      public = "https://www.w3.org/ns/activitystreams#Public" @@ -401,7 +417,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do             "to" => to,             "deleted_activity_id" => activity && activity.id           }, -         {:ok, activity} <- insert(data, local), +         {:ok, activity} <- insert(data, local, false), +         stream_out_participations(object, user),           _ <- decrease_replies_count_if_reply(object),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex new file mode 100644 index 000000000..2da3eac2f --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do +  alias Pleroma.User + +  require Logger + +  # has the user successfully posted before? +  defp old_user?(%User{} = u) do +    u.info.note_count > 0 || u.info.follower_count > 0 +  end + +  # does the post contain links? +  defp contains_links?(%{"content" => content} = _object) do +    content +    |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl") +    |> Floki.attribute("a", "href") +    |> length() > 0 +  end + +  defp contains_links?(_), do: false + +  def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do +    with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), +         {:contains_links, true} <- {:contains_links, contains_links?(object)}, +         {:old_user, true} <- {:old_user, old_user?(u)} do +      {:ok, message} +    else +      {:contains_links, false} -> +        {:ok, message} + +      {:old_user, false} -> +        {:reject, nil} + +      {:error, _} -> +        {:reject, nil} + +      e -> +        Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") +        {:reject, nil} +    end +  end + +  # in all other cases, pass through +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 8f1399ce6..a05e03263 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do        true      else        inbox_info = URI.parse(inbox) -      !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) +      !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host)      end    end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ff031a16e..3bb8b40b5 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -339,7 +339,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do      reply = Object.normalize(reply_id) -    if reply.data["type"] == "Question" and object["name"] do +    if reply && (reply.data["type"] == "Question" and object["name"]) do        Map.put(object, "type", "Answer")      else        object diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 10ff572a2..514266cee 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -151,16 +151,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def create_context(context) do      context = context || generate_id("contexts") -    changeset = Object.context_mapping(context) -    case Repo.insert(changeset) do -      {:ok, object} -> -        object +    # Ecto has problems accessing the constraint inside the jsonb, +    # so we explicitly check for the existed object before insert +    object = Object.get_cached_by_ap_id(context) -      # This should be solved by an upsert, but it seems ecto -      # has problems accessing the constraint inside the jsonb. -      {:error, _} -> -        Object.get_cached_by_ap_id(context) +    with true <- is_nil(object), +         changeset <- Object.context_mapping(context), +         {:ok, inserted_object} <- Repo.insert(changeset) do +      inserted_object +    else +      _ -> +        object      end    end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de2a13c01..498beb56a 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay    alias Pleroma.Web.AdminAPI.AccountView +  alias Pleroma.Web.AdminAPI.Config +  alias Pleroma.Web.AdminAPI.ConfigView    alias Pleroma.Web.AdminAPI.ReportView    alias Pleroma.Web.AdminAPI.Search    alias Pleroma.Web.CommonAPI @@ -362,6 +364,41 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end +  def config_show(conn, _params) do +    configs = Pleroma.Repo.all(Config) + +    conn +    |> put_view(ConfigView) +    |> render("index.json", %{configs: configs}) +  end + +  def config_update(conn, %{"configs" => configs}) do +    updated = +      if Pleroma.Config.get([:instance, :dynamic_configuration]) do +        updated = +          Enum.map(configs, fn +            %{"group" => group, "key" => key, "value" => value} -> +              {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) +              config + +            %{"group" => group, "key" => key, "delete" => "true"} -> +              {:ok, _} = Config.delete(%{group: group, key: key}) +              nil +          end) +          |> Enum.reject(&is_nil(&1)) + +        Pleroma.Config.TransferTask.load_and_update_env() +        Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"]) +        updated +      else +        [] +      end + +    conn +    |> put_view(ConfigView) +    |> render("index.json", %{configs: updated}) +  end +    def errors(conn, {:error, :not_found}) do      conn      |> put_status(404) diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex new file mode 100644 index 000000000..8b9b658a9 --- /dev/null +++ b/lib/pleroma/web/admin_api/config.ex @@ -0,0 +1,160 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.Config do +  use Ecto.Schema +  import Ecto.Changeset +  alias __MODULE__ +  alias Pleroma.Repo + +  @type t :: %__MODULE__{} + +  schema "config" do +    field(:key, :string) +    field(:group, :string) +    field(:value, :binary) + +    timestamps() +  end + +  @spec get_by_params(map()) :: Config.t() | nil +  def get_by_params(params), do: Repo.get_by(Config, params) + +  @spec changeset(Config.t(), map()) :: Changeset.t() +  def changeset(config, params \\ %{}) do +    config +    |> cast(params, [:key, :group, :value]) +    |> validate_required([:key, :group, :value]) +    |> unique_constraint(:key, name: :config_group_key_index) +  end + +  @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def create(params) do +    %Config{} +    |> changeset(Map.put(params, :value, transform(params[:value]))) +    |> Repo.insert() +  end + +  @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} +  def update(%Config{} = config, %{value: value}) do +    config +    |> change(value: transform(value)) +    |> Repo.update() +  end + +  @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def update_or_create(params) do +    with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do +      Config.update(config, params) +    else +      nil -> Config.create(params) +    end +  end + +  @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} +  def delete(params) do +    with %Config{} = config <- Config.get_by_params(params) do +      Repo.delete(config) +    else +      nil -> {:error, "Config with params #{inspect(params)} not found"} +    end +  end + +  @spec from_binary(binary()) :: term() +  def from_binary(value), do: :erlang.binary_to_term(value) + +  @spec from_binary_to_map(binary()) :: any() +  def from_binary_to_map(binary) do +    from_binary(binary) +    |> do_convert() +  end + +  defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1, +    do: %{k => do_convert(v)} + +  defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val)) + +  defp do_convert({k, v} = value) when is_tuple(value), +    do: %{k => do_convert(v)} + +  defp do_convert(value) when is_tuple(value), do: %{"tuple" => do_convert(Tuple.to_list(value))} + +  defp do_convert(value) when is_binary(value) or is_map(value) or is_number(value), do: value + +  defp do_convert(value) when is_atom(value) do +    string = to_string(value) + +    if String.starts_with?(string, "Elixir."), +      do: String.trim_leading(string, "Elixir."), +      else: value +  end + +  @spec transform(any()) :: binary() +  def transform(%{"tuple" => _} = entity), do: :erlang.term_to_binary(do_transform(entity)) + +  def transform(entity) when is_map(entity) do +    tuples = +      for {k, v} <- entity, +          into: [], +          do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)} + +    Enum.reject(tuples, fn {_k, v} -> is_nil(v) end) +    |> Enum.sort() +    |> :erlang.term_to_binary() +  end + +  def transform(entity) when is_list(entity) do +    list = Enum.map(entity, &do_transform(&1)) +    :erlang.term_to_binary(list) +  end + +  def transform(entity), do: :erlang.term_to_binary(entity) + +  defp do_transform(%Regex{} = value) when is_map(value), do: value + +  defp do_transform(%{"tuple" => [k, values] = entity}) when length(entity) == 2 do +    {do_transform(k), do_transform(values)} +  end + +  defp do_transform(%{"tuple" => values}) do +    Enum.reduce(values, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) +  end + +  defp do_transform(value) when is_map(value) do +    values = for {key, val} <- value, into: [], do: {String.to_atom(key), do_transform(val)} + +    Enum.sort(values) +  end + +  defp do_transform(value) when is_list(value) do +    Enum.map(value, &do_transform(&1)) +  end + +  defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity) + +  defp do_transform(value) when is_binary(value) do +    String.trim(value) +    |> do_transform_string() +  end + +  defp do_transform(value), do: value + +  defp do_transform_string(value) when byte_size(value) == 0, do: nil + +  defp do_transform_string(value) do +    cond do +      String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix") -> +        String.to_existing_atom("Elixir." <> value) + +      String.starts_with?(value, ":") -> +        String.replace(value, ":", "") |> String.to_existing_atom() + +      String.starts_with?(value, "i:") -> +        String.replace(value, "i:", "") |> String.to_integer() + +      true -> +        value +    end +  end +end diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex new file mode 100644 index 000000000..3ccc9ca46 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -0,0 +1,17 @@ +defmodule Pleroma.Web.AdminAPI.ConfigView do +  use Pleroma.Web, :view + +  def render("index.json", %{configs: configs}) do +    %{ +      configs: render_many(configs, __MODULE__, "show.json", as: :config) +    } +  end + +  def render("show.json", %{config: config}) do +    %{ +      key: config.key, +      group: config.group, +      value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value) +    } +  end +end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 47a73dc7e..e7db3a8ff 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.AdminAPI.ReportView do    use Pleroma.Web, :view    alias Pleroma.Activity +  alias Pleroma.HTML    alias Pleroma.User    alias Pleroma.Web.CommonAPI.Utils    alias Pleroma.Web.MastodonAPI.AccountView @@ -23,6 +24,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do      [account_ap_id | status_ap_ids] = report.data["object"]      account = User.get_cached_by_ap_id(account_ap_id) +    content = +      unless is_nil(report.data["content"]) do +        HTML.filter_tags(report.data["content"]) +      else +        nil +      end +      statuses =        Enum.map(status_ap_ids, fn ap_id ->          Activity.get_by_ap_id_with_object(ap_id) @@ -32,7 +40,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do        id: report.id,        account: AccountView.render("account.json", %{user: account}),        actor: AccountView.render("account.json", %{user: user}), -      content: report.data["content"], +      content: content,        created_at: created_at,        statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),        state: report.data["state"] diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f5193512e..f8df1e2ea 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -212,7 +212,7 @@ defmodule Pleroma.Web.CommonAPI do           cw <- data["spoiler_text"] || "",           sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),           full_payload <- String.trim(status <> cw), -         length when length in 1..limit <- String.length(full_payload), +         :ok <- validate_character_limit(full_payload, attachments, limit),           object <-             make_note_data(               user.ap_id, @@ -247,6 +247,8 @@ defmodule Pleroma.Web.CommonAPI do        res      else +      {:private_to_public, true} -> {:error, "The message visibility must be direct"} +      {:error, _} = e -> e        e -> {:error, e}      end    end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 6d82c0bd2..8b9477927 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -504,4 +504,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do        "inReplyTo" => object.data["id"]      }    end + +  def validate_character_limit(full_payload, attachments, limit) do +    length = String.length(full_payload) + +    if length < limit do +      if length > 0 or Enum.count(attachments) > 0 do +        :ok +      else +        {:error, "Cannot post an empty status without attachments"} +      end +    else +      {:error, "The status is over the character limit"} +    end +  end  end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 55706eeb8..8a753bb4f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do      |> put_status(status)      |> json(json)    end + +  @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil +  def fetch_integer_param(params, name, default \\ nil) do +    params +    |> Map.get(name, default) +    |> param_to_integer(default) +  end + +  defp param_to_integer(val, _) when is_integer(val), do: val + +  defp param_to_integer(val, default) when is_binary(val) do +    case Integer.parse(val) do +      {res, _} -> res +      _ -> default +    end +  end + +  defp param_to_integer(_, default), do: default  end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bd76e4295..ddaf88f1d 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -91,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do      Plug.Session,      store: :cookie,      key: cookie_name, -    signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, +    signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),      http_only: true,      secure: secure_cookies,      extra: extra diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 71e49494f..3db948c2e 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -15,7 +15,9 @@ defmodule Pleroma.Web.Federator.RetryQueue do    def start_link do      enabled = -      if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false) +      if Pleroma.Config.get(:env) == :test, +        do: true, +        else: Pleroma.Config.get([__MODULE__, :enabled], false)      if enabled do        Logger.info("Starting retry queue") @@ -219,7 +221,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do      {:noreply, state}    end -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      defp growth_function(_retries) do        _shutit = Pleroma.Config.get([__MODULE__, :initial_timeout])        DateTime.to_unix(DateTime.utc_now()) - 1 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 92cd77f62..7cdba4cc0 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -46,14 +46,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    require Logger -  plug( -    Pleroma.Plugs.RateLimitPlug, -    %{ -      max_requests: Config.get([:app_account_creation, :max_requests]), -      interval: Config.get([:app_account_creation, :interval]) -    } -    when action in [:account_register] -  ) +  plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register) +  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])    @local_mastodon_name "Mastodon-Local" @@ -142,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do            _ -> :error          end        end) +      |> add_if_present(params, "pleroma_background_image", :background, fn value -> +        with %Plug.Upload{} <- value, +             {:ok, object} <- ActivityPub.upload(value, type: :background) do +          {:ok, object.data} +        else +          _ -> :error +        end +      end)        |> Map.put(:emoji, user_info_emojis)      info_cng = User.Info.profile_update(user.info, info_params) @@ -166,8 +168,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def verify_credentials(%{assigns: %{user: user}} = conn, _) do +    chat_token = Phoenix.Token.sign(conn, "user socket", user.id) +      account = -      AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) +      AccountView.render("account.json", %{ +        user: user, +        for: user, +        with_pleroma_settings: true, +        with_chat_token: chat_token +      })      json(conn, account)    end @@ -445,12 +454,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  defp get_cached_vote_or_vote(user, object, choices) do +    idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + +    {_, res} = +      Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> +        case CommonAPI.vote(user, object, choices) do +          {:error, _message} = res -> {:ignore, res} +          res -> {:commit, res} +        end +      end) + +    res +  end +    def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do      with %Object{} = object <- Object.get_by_id(id),           true <- object.data["type"] == "Question",           %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),           true <- Visibility.visible_for_user?(activity, user), -         {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do +         {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do        conn        |> put_view(StatusView)        |> try_render("poll.json", %{object: object, for: user}) @@ -521,15 +544,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params) -      when length(media_ids) > 0 do -    params = -      params -      |> Map.put("status", ".") - -    post_status(conn, params) -  end -    def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do      params =        params @@ -547,18 +561,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        params = Map.drop(params, ["scheduled_at"]) -      case get_cached_status_or_post(conn, params) do -        {:ignore, message} -> -          conn -          |> put_status(422) -          |> json(%{error: message}) - +      case CommonAPI.post(user, params) do          {:error, message} ->            conn -          |> put_status(422) +          |> put_status(:unprocessable_entity)            |> json(%{error: message}) -        {_, activity} -> +        {:ok, activity} ->            conn            |> put_view(StatusView)            |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -566,21 +575,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do -    idempotency_key = -      case get_req_header(conn, "idempotency-key") do -        [key] -> key -        _ -> Ecto.UUID.generate() -      end - -    Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> -      case CommonAPI.post(user, params) do -        {:ok, activity} -> activity -        {:error, message} -> {:ignore, message} -      end -    end) -  end -    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do        json(conn, %{}) @@ -830,7 +824,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        conn        |> put_view(AccountView) -      |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user}) +      |> render("accounts.json", %{for: user, users: users, as: :user})      else        _ -> json(conn, [])      end @@ -1124,58 +1118,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) -    statuses = Activity.search(user, query) -    tags_path = Web.base_url() <> "/tag/" - -    tags = -      query -      |> String.split() -      |> Enum.uniq() -      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) -      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) -      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - -    res = %{ -      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), -      "statuses" => -        StatusView.render("index.json", activities: statuses, for: user, as: :activity), -      "hashtags" => tags -    } - -    json(conn, res) -  end - -  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) -    statuses = Activity.search(user, query) - -    tags = -      query -      |> String.split() -      |> Enum.uniq() -      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) -      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - -    res = %{ -      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), -      "statuses" => -        StatusView.render("index.json", activities: statuses, for: user, as: :activity), -      "hashtags" => tags -    } - -    json(conn, res) -  end - -  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do -    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - -    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - -    json(conn, res) -  end -    def favourites(%{assigns: %{user: user}} = conn, params) do      params =        params diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex new file mode 100644 index 000000000..0d1e2355d --- /dev/null +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do +  use Pleroma.Web, :controller +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Web +  alias Pleroma.Web.MastodonAPI.AccountView +  alias Pleroma.Web.MastodonAPI.StatusView + +  alias Pleroma.Web.ControllerHelper + +  require Logger + +  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) + +  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do +    accounts = User.search(query, search_options(params, user)) +    statuses = Activity.search(user, query) +    tags_path = Web.base_url() <> "/tag/" + +    tags = +      query +      |> String.split() +      |> Enum.uniq() +      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) +      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) +      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) + +    res = %{ +      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), +      "statuses" => +        StatusView.render("index.json", activities: statuses, for: user, as: :activity), +      "hashtags" => tags +    } + +    json(conn, res) +  end + +  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do +    accounts = User.search(query, search_options(params, user)) +    statuses = Activity.search(user, query) + +    tags = +      query +      |> String.split() +      |> Enum.uniq() +      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) +      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + +    res = %{ +      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), +      "statuses" => +        StatusView.render("index.json", activities: statuses, for: user, as: :activity), +      "hashtags" => tags +    } + +    json(conn, res) +  end + +  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do +    accounts = User.search(query, search_options(params, user)) +    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + +    json(conn, res) +  end + +  defp search_options(params, user) do +    [ +      resolve: params["resolve"] == "true", +      following: params["following"] == "true", +      limit: ControllerHelper.fetch_integer_param(params, "limit"), +      offset: ControllerHelper.fetch_integer_param(params, "offset"), +      for_user: user +    ] +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b91726b45..62c516f8e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -66,6 +66,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    end    defp do_render("account.json", %{user: user} = opts) do +    display_name = HTML.strip_tags(user.name || user.nickname) +      image = User.avatar_url(user) |> MediaProxy.url()      header = User.banner_url(user) |> MediaProxy.url()      user_info = User.get_cached_user_info(user) @@ -96,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        id: to_string(user.id),        username: username_from_nickname(user.nickname),        acct: user.nickname, -      display_name: user.name || user.nickname, +      display_name: display_name,        locked: user_info.locked,        created_at: Utils.to_masto_date(user.inserted_at),        followers_count: user_info.follower_count, @@ -125,13 +127,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          hide_follows: user.info.hide_follows,          hide_favorites: user.info.hide_favorites,          relationship: relationship, -        skip_thread_containment: user.info.skip_thread_containment +        skip_thread_containment: user.info.skip_thread_containment, +        background_image: image_url(user.info.background) |> MediaProxy.url()        }      }      |> maybe_put_role(user, opts[:for])      |> maybe_put_settings(user, opts[:for], user_info)      |> maybe_put_notification_settings(user, opts[:for])      |> maybe_put_settings_store(user, opts[:for], opts) +    |> maybe_put_chat_token(user, opts[:for], opts)    end    defp username_from_nickname(string) when is_binary(string) do @@ -163,6 +167,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_settings_store(data, _, _, _), do: data +  defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ +         with_chat_token: token +       }) do +    data +    |> Kernel.put_in([:pleroma, :chat_token], token) +  end + +  defp maybe_put_chat_token(data, _, _, _), do: data +    defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do      data      |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) @@ -182,4 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    end    defp maybe_put_notification_settings(data, _, _), do: data + +  defp image_url(%{"url" => [%{"href" => href} | _]}), do: href +  defp image_url(_), do: nil  end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index abfa26754..3299e1721 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do      "public:media",      "public:local:media",      "user", +    "user:notification",      "direct",      "list",      "hashtag" diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 18973413e..d53e20d12 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -76,14 +76,16 @@ defmodule Pleroma.Web.OAuth.Authorization do    def use_token(%Authorization{used: true}), do: {:error, "already used"}    @spec delete_user_authorizations(User.t()) :: {integer(), any()} -  def delete_user_authorizations(%User{id: user_id}) do -    from( -      a in Pleroma.Web.OAuth.Authorization, -      where: a.user_id == ^user_id -    ) +  def delete_user_authorizations(%User{} = user) do +    user +    |> delete_by_user_query      |> Repo.delete_all()    end +  def delete_by_user_query(%User{id: user_id}) do +    from(a in __MODULE__, where: a.user_id == ^user_id) +  end +    @doc "gets auth for app by token"    @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}    def get_by_token(%App{id: app_id} = _app, token) do diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 79d803295..3f8e3b074 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,6 +5,7 @@  defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller +  alias Pleroma.Helpers.UriHelper    alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User @@ -26,34 +27,25 @@ defmodule Pleroma.Web.OAuth.OAuthController do    action_fallback(Pleroma.Web.OAuth.FallbackController) +  @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" +    # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg -  def authorize(conn, %{"authorization" => _} = params) do +  def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do      {auth_attrs, params} = Map.pop(params, "authorization")      authorize(conn, Map.merge(params, auth_attrs))    end -  def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do +  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do      if ControllerHelper.truthy_param?(params["force_login"]) do        do_authorize(conn, params)      else -      redirect_uri = -        if is_binary(params["redirect_uri"]) do -          params["redirect_uri"] -        else -          app = Repo.preload(token, :app).app - -          app.redirect_uris -          |> String.split() -          |> Enum.at(0) -        end - -      redirect(conn, external: redirect_uri(conn, redirect_uri)) +      handle_existing_authorization(conn, params)      end    end -  def authorize(conn, params), do: do_authorize(conn, params) +  def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) -  defp do_authorize(conn, params) do +  defp do_authorize(%Plug.Conn{} = conn, params) do      app = Repo.get_by(App, client_id: params["client_id"])      available_scopes = (app && app.scopes) || []      scopes = Scopes.fetch_scopes(params, available_scopes) @@ -70,8 +62,41 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end +  defp handle_existing_authorization( +         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, +         %{"redirect_uri" => @oob_token_redirect_uri} +       ) do +    render(conn, "oob_token_exists.html", %{token: token}) +  end + +  defp handle_existing_authorization( +         %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, +         %{} = params +       ) do +    app = Repo.preload(token, :app).app + +    redirect_uri = +      if is_binary(params["redirect_uri"]) do +        params["redirect_uri"] +      else +        default_redirect_uri(app) +      end + +    if redirect_uri in String.split(app.redirect_uris) do +      redirect_uri = redirect_uri(conn, redirect_uri) +      url_params = %{access_token: token.token} +      url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) +      url = UriHelper.append_uri_params(redirect_uri, url_params) +      redirect(conn, external: url) +    else +      conn +      |> put_flash(:error, "Unlisted redirect_uri.") +      |> redirect(external: redirect_uri(conn, redirect_uri)) +    end +  end +    def create_authorization( -        conn, +        %Plug.Conn{} = conn,          %{"authorization" => _} = params,          opts \\ []        ) do @@ -83,35 +108,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def after_create_authorization(conn, auth, %{ -        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs +  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ +        "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}        }) do -    redirect_uri = redirect_uri(conn, redirect_uri) - -    if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do -      render(conn, "results.html", %{ -        auth: auth -      }) -    else -      connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" -      url = "#{redirect_uri}#{connector}" -      url_params = %{:code => auth.token} - -      url_params = -        if auth_attrs["state"] do -          Map.put(url_params, :state, auth_attrs["state"]) -        else -          url_params -        end - -      url = "#{url}#{Plug.Conn.Query.encode(url_params)}" +    render(conn, "oob_authorization_created.html", %{auth: auth}) +  end +  def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ +        "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs +      }) do +    app = Repo.preload(auth, :app).app + +    # An extra safety measure before we redirect (also done in `do_create_authorization/2`) +    if redirect_uri in String.split(app.redirect_uris) do +      redirect_uri = redirect_uri(conn, redirect_uri) +      url_params = %{code: auth.token} +      url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) +      url = UriHelper.append_uri_params(redirect_uri, url_params)        redirect(conn, external: url) +    else +      conn +      |> put_flash(:error, "Unlisted redirect_uri.") +      |> redirect(external: redirect_uri(conn, redirect_uri))      end    end    defp handle_create_authorization_error( -         conn, +         %Plug.Conn{} = conn,           {:error, scopes_issue},           %{"authorization" => _} = params         ) @@ -125,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    defp handle_create_authorization_error( -         conn, +         %Plug.Conn{} = conn,           {:auth_active, false},           %{"authorization" => _} = params         ) do @@ -137,13 +160,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> authorize(params)    end -  defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do +  defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do      Authenticator.handle_error(conn, error)    end    @doc "Renew access_token with refresh_token"    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "refresh_token", "refresh_token" => token} = _params        ) do      with {:ok, app} <- Token.Utils.fetch_app(conn), @@ -159,7 +182,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do +  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           fixed_token = Token.Utils.fix_padding(params["code"]),           {:ok, auth} <- Authorization.get_by_token(app, fixed_token), @@ -176,7 +199,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "password"} = params        ) do      with {:ok, %User{} = user} <- Authenticator.get_user(conn), @@ -207,7 +230,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    def token_exchange( -        conn, +        %Plug.Conn{} = conn,          %{"grant_type" => "password", "name" => name, "password" => _password} = params        ) do      params = @@ -218,7 +241,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      token_exchange(conn, params)    end -  def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do +  def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, auth} <- Authorization.create_authorization(app, %User{}),           {:ok, token} <- Token.exchange_token(app, auth) do @@ -231,9 +254,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    # Bad request -  def token_exchange(conn, params), do: bad_request(conn, params) +  def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) -  def token_revoke(conn, %{"token" => _token} = params) do +  def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do      with {:ok, app} <- Token.Utils.fetch_app(conn),           {:ok, _token} <- RevokeToken.revoke(app, params) do        json(conn, %{}) @@ -244,17 +267,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def token_revoke(conn, params), do: bad_request(conn, params) +  def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)    # Response for bad request -  defp bad_request(conn, _) do +  defp bad_request(%Plug.Conn{} = conn, _) do      conn      |> put_status(500)      |> json(%{error: "Bad request"})    end    @doc "Prepares OAuth request to provider for Ueberauth" -  def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do +  def prepare_request(%Plug.Conn{} = conn, %{ +        "provider" => provider, +        "authorization" => auth_attrs +      }) do      scope =        auth_attrs        |> Scopes.fetch_scopes([]) @@ -275,7 +301,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      redirect(conn, to: o_auth_path(conn, :request, provider, params))    end -  def request(conn, params) do +  def request(%Plug.Conn{} = conn, params) do      message =        if params["provider"] do          "Unsupported OAuth provider: #{params["provider"]}." @@ -288,7 +314,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> redirect(to: "/")    end -  def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do +  def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do      params = callback_params(params)      messages = for e <- Map.get(failure, :errors, []), do: e.message      message = Enum.join(messages, "; ") @@ -298,7 +324,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> redirect(external: redirect_uri(conn, params["redirect_uri"]))    end -  def callback(conn, params) do +  def callback(%Plug.Conn{} = conn, params) do      params = callback_params(params)      with {:ok, registration} <- Authenticator.get_registration(conn) do @@ -316,7 +342,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do              })            conn -          |> put_session(:registration_id, registration.id) +          |> put_session_registration_id(registration.id)            |> registration_details(%{"authorization" => registration_params})        end      else @@ -333,7 +359,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      Map.merge(params, Jason.decode!(state))    end -  def registration_details(conn, %{"authorization" => auth_attrs}) do +  def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do      render(conn, "register.html", %{        client_id: auth_attrs["client_id"],        redirect_uri: auth_attrs["redirect_uri"], @@ -344,7 +370,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      })    end -  def register(conn, %{"authorization" => _, "op" => "connect"} = params) do +  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do      with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),           %Registration{} = registration <- Repo.get(Registration, registration_id),           {_, {:ok, auth}} <- @@ -363,7 +389,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end -  def register(conn, %{"authorization" => _, "op" => "register"} = params) do +  def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do      with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),           %Registration{} = registration <- Repo.get(Registration, registration_id),           {:ok, user} <- Authenticator.create_from_registration(conn, registration) do @@ -399,7 +425,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    defp do_create_authorization( -         conn, +         %Plug.Conn{} = conn,           %{             "authorization" =>               %{ @@ -420,13 +446,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do    end    # Special case: Local MastodonFE -  defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) +  defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login) -  defp redirect_uri(_conn, redirect_uri), do: redirect_uri +  defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri -  defp get_session_registration_id(conn), do: get_session(conn, :registration_id) +  defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) -  defp put_session_registration_id(conn, registration_id), +  defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),      do: put_session(conn, :registration_id, registration_id)    @spec validate_scopes(App.t(), map()) :: @@ -436,4 +462,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do      |> Scopes.fetch_scopes(app.scopes)      |> Scopes.validates(app.scopes)    end + +  def default_redirect_uri(%App{} = app) do +    app.redirect_uris +    |> String.split() +    |> Enum.at(0) +  end  end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f412f7eb2..90c304487 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.OAuth.Token.Query -  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)    @type t :: %__MODULE__{}    schema "oauth_tokens" do @@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do    defp put_valid_until(changeset, attrs) do      expires_in = -      Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) +      Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in()))      changeset      |> change(%{valid_until: expires_in}) @@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do    end    def is_expired?(_), do: false + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 64e78b183..2648571ad 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -4,15 +4,13 @@ defmodule Pleroma.Web.OAuth.Token.Response do    alias Pleroma.User    alias Pleroma.Web.OAuth.Token.Utils -  @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) -    @doc false    def build(%User{} = user, token, opts \\ %{}) do      %{        token_type: "Bearer",        access_token: token.token,        refresh_token: token.refresh_token, -      expires_in: @expires_in, +      expires_in: expires_in(),        scope: Enum.join(token.scopes, " "),        me: user.ap_id      } @@ -25,8 +23,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do        access_token: token.token,        refresh_token: token.refresh_token,        created_at: Utils.format_created_at(token), -      expires_in: @expires_in, +      expires_in: expires_in(),        scope: Enum.join(token.scopes, " ")      }    end + +  defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)  end diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 26eb614a6..d376e2069 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.RelMe do      with_body: true    ] -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      def parse(url) when is_binary(url), do: parse_url(url)    else      def parse(url) when is_binary(url) do diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 9bc8f2559..6506de46c 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -4,25 +4,53 @@  defmodule Pleroma.Web.RichMedia.Helpers do    alias Pleroma.Activity +  alias Pleroma.Config    alias Pleroma.HTML    alias Pleroma.Object    alias Pleroma.Web.RichMedia.Parser +  @spec validate_page_url(any()) :: :ok | :error    defp validate_page_url(page_url) when is_binary(page_url) do -    if AutoLinker.Parser.is_url?(page_url, true) do -      URI.parse(page_url) |> validate_page_url -    else -      :error +    validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] + +    page_url +    |> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld) +    |> parse_uri(page_url) +  end + +  defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) +       when scheme == "https" and not is_nil(authority) do +    cond do +      host in Config.get([:rich_media, :ignore_hosts], []) -> +        :error + +      get_tld(host) in Config.get([:rich_media, :ignore_tld], []) -> +        :error + +      true -> +        :ok      end    end -  defp validate_page_url(%URI{authority: nil}), do: :error -  defp validate_page_url(%URI{scheme: nil}), do: :error -  defp validate_page_url(%URI{}), do: :ok    defp validate_page_url(_), do: :error +  defp parse_uri(true, url) do +    url +    |> URI.parse() +    |> validate_page_url +  end + +  defp parse_uri(_, _), do: :error + +  defp get_tld(host) do +    host +    |> String.split(".") +    |> Enum.reverse() +    |> hd +  end +    def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do -    with true <- Pleroma.Config.get([:rich_media, :enabled]), +    with true <- Config.get([:rich_media, :enabled]),           %Object{} = object <- Object.normalize(activity),           false <- object.data["sensitive"] || false,           {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index e4595800c..21cd47890 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.RichMedia.Parser do    def parse(nil), do: {:error, "No URL provided"} -  if Mix.env() == :test do +  if Pleroma.Config.get(:env) == :test do      def parse(url), do: parse_url(url)    else      def parse(url) do diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index 4a7c5eae0..fb79630e4 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -1,15 +1,19 @@  defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do    def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do -    with elements = [_ | _] <- get_elements(html, key_name, prefix), -         meta_data = -           Enum.reduce(elements, data, fn el, acc -> -             attributes = normalize_attributes(el, prefix, key_name, value_name) +    meta_data = +      html +      |> get_elements(key_name, prefix) +      |> Enum.reduce(data, fn el, acc -> +        attributes = normalize_attributes(el, prefix, key_name, value_name) -             Map.merge(acc, attributes) -           end) do -      {:ok, meta_data} +        Map.merge(acc, attributes) +      end) +      |> maybe_put_title(html) + +    if Enum.empty?(meta_data) do +      {:error, error_message}      else -      _e -> {:error, error_message} +      {:ok, meta_data}      end    end @@ -27,4 +31,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do      %{String.to_atom(data[key_name]) => data[value_name]}    end + +  defp maybe_put_title(%{title: _} = meta, _), do: meta + +  defp maybe_put_title(meta, html) when meta != %{} do +    case get_page_title(html) do +      "" -> meta +      title -> Map.put_new(meta, :title, title) +    end +  end + +  defp maybe_put_title(meta, _), do: meta + +  defp get_page_title(html) do +    Floki.find(html, "title") |> Floki.text() +  end  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ddab99254..9cb8db7fd 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.UserEnabledPlug)      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureUserKeyPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :authenticated_api do @@ -41,6 +42,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.UserEnabledPlug)      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :admin_api do @@ -57,6 +59,7 @@ defmodule Pleroma.Web.Router do      plug(Pleroma.Plugs.SetUserSessionIdPlug)      plug(Pleroma.Plugs.EnsureAuthenticatedPlug)      plug(Pleroma.Plugs.UserIsAdminPlug) +    plug(Pleroma.Plugs.IdempotencyPlug)    end    pipeline :mastodon_html do @@ -133,8 +136,8 @@ defmodule Pleroma.Web.Router do    scope "/api/pleroma", Pleroma.Web.TwitterAPI do      pipe_through(:pleroma_api) -    get("/password_reset/:token", UtilController, :show_password_reset) -    post("/password_reset", UtilController, :password_reset) +    get("/password_reset/:token", PasswordController, :reset, as: :reset_password) +    post("/password_reset", PasswordController, :do_reset, as: :reset_password)      get("/emoji", UtilController, :emoji)      get("/captcha", UtilController, :captcha)      get("/healthcheck", UtilController, :healthcheck) @@ -202,6 +205,9 @@ defmodule Pleroma.Web.Router do      put("/statuses/:id", AdminAPIController, :status_update)      delete("/statuses/:id", AdminAPIController, :status_delete) + +    get("/config", AdminAPIController, :config_show) +    post("/config", AdminAPIController, :config_update)    end    scope "/", Pleroma.Web.TwitterAPI do @@ -412,7 +418,7 @@ defmodule Pleroma.Web.Router do      get("/trends", MastodonAPIController, :empty_array) -    get("/accounts/search", MastodonAPIController, :account_search) +    get("/accounts/search", SearchController, :account_search)      scope [] do        pipe_through(:oauth_read_or_public) @@ -431,7 +437,7 @@ defmodule Pleroma.Web.Router do        get("/accounts/:id/following", MastodonAPIController, :following)        get("/accounts/:id", MastodonAPIController, :user) -      get("/search", MastodonAPIController, :search) +      get("/search", SearchController, :search)        get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)      end @@ -439,7 +445,7 @@ defmodule Pleroma.Web.Router do    scope "/api/v2", Pleroma.Web.MastodonAPI do      pipe_through([:api, :oauth_read_or_public]) -    get("/search", MastodonAPIController, :search2) +    get("/search", SearchController, :search2)    end    scope "/api", Pleroma.Web do @@ -606,12 +612,6 @@ defmodule Pleroma.Web.Router do      get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)    end -  scope "/", Pleroma.Web do -    pipe_through(:oembed) - -    get("/oembed", OEmbed.OEmbedController, :url) -  end -    pipeline :activitypub do      plug(:accepts, ["activity+json", "json"])      plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -701,7 +701,7 @@ defmodule Pleroma.Web.Router do      get("/:sig/:url/:filename", MediaProxyController, :remote)    end -  if Mix.env() == :dev do +  if Pleroma.Config.get(:env) == :dev do      scope "/dev" do        pipe_through([:mailbox_preview]) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 9e91a5a40..e96e4e1e4 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -146,7 +146,7 @@ defmodule Pleroma.Web.Salmon do          do: Instances.set_reachable(url)        Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) -      :ok +      {:ok, code}      else        e ->          unless params[:unreachable_since], do: Instances.set_reachable(url) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index a23f80f26..4f325113a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -110,23 +110,18 @@ defmodule Pleroma.Web.Streamer do      {:noreply, topics}    end -  def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do -    topic = "user:#{item.user_id}" - -    Enum.each(topics[topic] || [], fn socket -> -      json = -        %{ -          event: "notification", -          payload: -            NotificationView.render("show.json", %{ -              notification: item, -              for: socket.assigns["user"] -            }) -            |> Jason.encode!() -        } -        |> Jason.encode!() - -      send(socket.transport_pid, {:text, json}) +  def handle_cast( +        %{action: :stream, topic: topic, item: %Notification{} = item}, +        topics +      ) +      when topic in ["user", "user:notification"] do +    topics +    |> Map.get("#{topic}:#{item.user_id}", []) +    |> Enum.each(fn socket -> +      send( +        socket.transport_pid, +        {:text, represent_notification(socket.assigns[:user], item)} +      )      end)      {:noreply, topics} @@ -216,6 +211,20 @@ defmodule Pleroma.Web.Streamer do      |> Jason.encode!()    end +  @spec represent_notification(User.t(), Notification.t()) :: binary() +  defp represent_notification(%User{} = user, %Notification{} = notify) do +    %{ +      event: "notification", +      payload: +        NotificationView.render( +          "show.json", +          %{notification: notify, for: user} +        ) +        |> Jason.encode!() +    } +    |> Jason.encode!() +  end +    def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do      Enum.each(topics[topic] || [], fn socket ->        # Get the current user so we have up-to-date blocks etc. @@ -274,7 +283,7 @@ defmodule Pleroma.Web.Streamer do      end)    end -  defp internal_topic(topic, socket) when topic in ~w[user direct] do +  defp internal_topic(topic, socket) when topic in ~w[user user:notification direct] do      "#{topic}:#{socket.assigns[:user].id}"    end diff --git a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index 8443d906b..8443d906b 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex new file mode 100644 index 000000000..961aad976 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -0,0 +1,2 @@ +<h1>Authorization exists</h1> +<h2>Access token is <%= @token.token %></h2> diff --git a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex index ee84750c7..ee84750c7 100644 --- a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index a3facf017..7d3ef6b0d 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,5 +1,5 @@  <h2>Password Reset for <%= @user.nickname %></h2> -<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %> +<%= form_for @conn, reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>    <div class="form-row">      <%= label f, :password, "Password" %>      <%= password_input f, :password %> diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex index df037c01e..df037c01e 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex index f30ba3274..f30ba3274 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex new file mode 100644 index 000000000..1941e6143 --- /dev/null +++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordController do +  @moduledoc """ +  The module containts functions for reset password. +  """ + +  use Pleroma.Web, :controller + +  require Logger + +  alias Pleroma.PasswordResetToken +  alias Pleroma.Repo +  alias Pleroma.User + +  def reset(conn, %{"token" => token}) do +    with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), +         %User{} = user <- User.get_cached_by_id(token.user_id) do +      render(conn, "reset.html", %{ +        token: token, +        user: user +      }) +    else +      _e -> render(conn, "invalid_token.html") +    end +  end + +  def do_reset(conn, %{"data" => data}) do +    with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do +      render(conn, "reset_success.html") +    else +      _e -> render(conn, "reset_failed.html") +    end +  end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 489170d80..b1863528f 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Activity    alias Pleroma.Emoji    alias Pleroma.Notification -  alias Pleroma.PasswordResetToken -  alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub @@ -20,26 +18,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    alias Pleroma.Web.OStatus    alias Pleroma.Web.WebFinger -  def show_password_reset(conn, %{"token" => token}) do -    with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), -         %User{} = user <- User.get_cached_by_id(token.user_id) do -      render(conn, "password_reset.html", %{ -        token: token, -        user: user -      }) -    else -      _e -> render(conn, "invalid_token.html") -    end -  end - -  def password_reset(conn, %{"data" => data}) do -    with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do -      render(conn, "password_reset_success.html") -    else -      _e -> render(conn, "password_reset_failed.html") -    end -  end -    def help_test(conn, _params) do      json(conn, "ok")    end diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex new file mode 100644 index 000000000..b166b925d --- /dev/null +++ b/lib/pleroma/web/twitter_api/views/password_view.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordView do +  use Pleroma.Web, :view +  import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex index f4c04131c..5cb8669fe 100644 --- a/lib/pleroma/web/views/error_view.ex +++ b/lib/pleroma/web/views/error_view.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ErrorView do    def render("500.json", assigns) do      Logger.error("Internal server error: #{inspect(assigns[:reason])}") -    if Mix.env() != :prod do +    if Pleroma.Config.get(:env) != :prod do        %{errors: %{detail: "Internal server error", reason: inspect(assigns[:reason])}}      else        %{errors: %{detail: "Internal server error"}}  | 
