From 2c04e1cf3d31522453be62ddc9198d957da9c429 Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sat, 2 Nov 2019 09:14:03 -0400 Subject: [PATCH] filter editor --- config/config.exs | 2 +- lib/philomena/filters.ex | 4 +- lib/philomena/filters/filter.ex | 109 +++++++++++++++++- lib/philomena/users/encryptor.ex | 15 +++ lib/philomena/users/user.ex | 12 +- .../controllers/filter_controller.ex | 2 +- lib/philomena_web/plugs/current_filter.ex | 3 +- .../templates/filter/_form.html.slime | 56 +++++++++ .../templates/filter/edit.html.slime | 2 + .../templates/filter/index.html.slime | 9 +- .../templates/filter/new.html.slime | 2 + .../templates/tag/_tag_editor.html.slime | 9 ++ .../phoenix/controller_callbacks.ex | 24 ++-- lib/pow_multi_factor/plug.ex | 29 +++-- 14 files changed, 239 insertions(+), 39 deletions(-) create mode 100644 lib/philomena_web/templates/filter/_form.html.slime create mode 100644 lib/philomena_web/templates/filter/edit.html.slime create mode 100644 lib/philomena_web/templates/filter/new.html.slime create mode 100644 lib/philomena_web/templates/tag/_tag_editor.html.slime diff --git a/config/config.exs b/config/config.exs index e05788d9..7c0fda34 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,7 +18,7 @@ config :philomena, :pow, user: Philomena.Users.User, repo: Philomena.Repo, web_module: PhilomenaWeb, - extensions: [PowResetPassword, PowPersistentSession, PowMultiFactor], + extensions: [PowResetPassword, PowPersistentSession], controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks config :bcrypt_elixir, diff --git a/lib/philomena/filters.ex b/lib/philomena/filters.ex index 5402b185..81ff15ca 100644 --- a/lib/philomena/filters.ex +++ b/lib/philomena/filters.ex @@ -64,8 +64,8 @@ defmodule Philomena.Filters do {:error, %Ecto.Changeset{}} """ - def create_filter(attrs \\ %{}) do - %Filter{} + def create_filter(user, attrs \\ %{}) do + %Filter{user_id: user.id} |> Filter.changeset(attrs) |> Repo.insert() end diff --git a/lib/philomena/filters/filter.ex b/lib/philomena/filters/filter.ex index bfffbc50..4025262b 100644 --- a/lib/philomena/filters/filter.ex +++ b/lib/philomena/filters/filter.ex @@ -1,9 +1,15 @@ defmodule Philomena.Filters.Filter do use Ecto.Schema import Ecto.Changeset + import Ecto.Query + + alias Philomena.Tags.Tag + alias Philomena.Images.Query + alias Philomena.Users.User + alias Philomena.Repo schema "filters" do - belongs_to :user, Philomena.Users.User + belongs_to :user, User field :name, :string field :description, :string @@ -15,13 +21,110 @@ defmodule Philomena.Filters.Filter do field :spoilered_tag_ids, {:array, :integer}, default: [] field :user_count, :integer, default: 0 + field :spoilered_tag_list, :string, virtual: true + field :hidden_tag_list, :string, virtual: true + timestamps(inserted_at: :created_at) end @doc false def changeset(filter, attrs) do filter - |> cast(attrs, []) - |> validate_required([]) + |> cast(attrs, [:spoilered_tag_list, :hidden_tag_list, :description, :name, :spoilered_complex_str, :hidden_complex_str]) + |> propagate_tag_lists() + |> validate_required([:name]) + |> unsafe_validate_unique([:user_id, :name], Repo) + |> validate_my_downvotes(:spoilered_complex_str) + |> validate_my_downvotes(:hidden_complex_str) + |> validate_search(:spoilered_complex_str) + |> validate_search(:hidden_complex_str) + end + + def assign_tag_lists(filter) do + tags = Enum.uniq(filter.spoilered_tag_ids ++ filter.hidden_tag_ids) + + lookup = + Tag + |> where([t], t.id in ^tags) + |> Repo.all() + |> Map.new(fn t -> {t.id, t.name} end) + + spoilered_tag_list = + filter.spoilered_tag_ids + |> Enum.map(&lookup[&1]) + |> Enum.filter(& &1 != nil) + |> Enum.sort() + |> Enum.join(", ") + + hidden_tag_list = + filter.hidden_tag_ids + |> Enum.map(&lookup[&1]) + |> Enum.filter(& &1 != nil) + |> Enum.sort() + |> Enum.join(", ") + + %{filter | hidden_tag_list: hidden_tag_list, spoilered_tag_list: spoilered_tag_list} + end + + defp propagate_tag_lists(changeset) do + spoilers = get_field(changeset, :spoilered_tag_list) |> parse_tag_list + filters = get_field(changeset, :hidden_tag_list) |> parse_tag_list + tags = Enum.uniq(spoilers ++ filters) + + lookup = + Tag + |> where([t], t.name in ^tags) + |> Repo.all() + |> Map.new(fn t -> {t.name, t.id} end) + + spoilered_tag_ids = + spoilers + |> Enum.map(&lookup[&1]) + |> Enum.filter(& &1 != nil) + + hidden_tag_ids = + filters + |> Enum.map(&lookup[&1]) + |> Enum.filter(& &1 != nil) + + changeset + |> put_change(:spoilered_tag_ids, spoilered_tag_ids) + |> put_change(:hidden_tag_ids, hidden_tag_ids) + end + + defp validate_my_downvotes(changeset, field) do + value = get_field(changeset, field) || "" + + if String.match?(value, ~r/my:downvotes/i) do + changeset + |> add_error(field, "cannot contain my:downvotes") + else + changeset + end + end + + defp validate_search(changeset, field) do + user_id = get_field(changeset, :user_id) + + if user_id do + user = User |> Repo.get!(user_id) + output = Query.user_parser(user, get_field(changeset, field)) + + case output do + {:ok, _} -> changeset + _ -> + changeset + |> add_error(field, "is invalid") + end + else + changeset + end + end + + defp parse_tag_list(list) do + (list || "") + |> String.split(",") + |> Enum.map(&String.trim(&1)) + |> Enum.filter(& &1 != "") end end diff --git a/lib/philomena/users/encryptor.ex b/lib/philomena/users/encryptor.ex index 6458bc4b..9a17c96f 100644 --- a/lib/philomena/users/encryptor.ex +++ b/lib/philomena/users/encryptor.ex @@ -13,6 +13,21 @@ defmodule Philomena.Users.Encryptor do :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, msg, "", auth_tag, false) end + def encrypt_model(secret) do + salt = :crypto.strong_rand_bytes(16) + iv = :crypto.strong_rand_bytes(12) + + {:ok, key} = :pbkdf2.pbkdf2(:sha, otp_secret(), salt, 2000, 32) + {msg, auth_tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, secret, "", true) + + # attr_encrypted encoding scheme + %{ + secret: Base.encode64(<>) <> "\n", + salt: "_" <> Base.encode64(salt) <> "\n", + iv: Base.encode64(iv) <> "\n" + } + end + defp otp_secret do Application.get_env(:philomena, :otp_secret_key) end diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index 612310f8..05c04e9f 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -106,7 +106,6 @@ defmodule Philomena.Users.User do |> pow_extension_changeset(attrs) |> cast(attrs, []) |> validate_required([]) - |> put_otp_secret() end def otp_secret(%{encrypted_otp_secret: x} = user) when x not in [nil, ""] do @@ -119,9 +118,14 @@ defmodule Philomena.Users.User do def otp_secret(_user), do: nil - defp put_otp_secret(%{valid?: true} = changeset) do + def put_otp_secret(user_or_changeset, secret) do + data = Philomena.Users.Encryptor.encrypt_model(secret) + user_or_changeset + |> change(%{ + encrypted_otp_secret: data.secret, + encrypted_otp_secret_iv: data.iv, + encrypted_otp_secret_salt: data.salt + }) end - - defp put_otp_secret(changeset), do: changeset end diff --git a/lib/philomena_web/controllers/filter_controller.ex b/lib/philomena_web/controllers/filter_controller.ex index e4aabab0..338171f4 100644 --- a/lib/philomena_web/controllers/filter_controller.ex +++ b/lib/philomena_web/controllers/filter_controller.ex @@ -67,7 +67,7 @@ defmodule PhilomenaWeb.FilterController do end def edit(conn, _params) do - filter = conn.assigns.filter + filter = conn.assigns.filter |> Filter.assign_tag_lists() changeset = Filters.change_filter(filter) render(conn, "edit.html", filter: filter, changeset: changeset) diff --git a/lib/philomena_web/plugs/current_filter.ex b/lib/philomena_web/plugs/current_filter.ex index 4ef1cd1b..416d2e96 100644 --- a/lib/philomena_web/plugs/current_filter.ex +++ b/lib/philomena_web/plugs/current_filter.ex @@ -1,6 +1,5 @@ defmodule PhilomenaWeb.Plugs.CurrentFilter do import Plug.Conn - import Ecto.Query alias Philomena.{Filters, Filters.Filter} alias Philomena.Repo @@ -22,7 +21,7 @@ defmodule PhilomenaWeb.Plugs.CurrentFilter do filter = if filter_id, do: Repo.get(Filter, filter_id) - filter = filter || Filters.default_filter() + filter || Filters.default_filter() end conn diff --git a/lib/philomena_web/templates/filter/_form.html.slime b/lib/philomena_web/templates/filter/_form.html.slime new file mode 100644 index 00000000..b8e64ff3 --- /dev/null +++ b/lib/philomena_web/templates/filter/_form.html.slime @@ -0,0 +1,56 @@ +.form + = form_for @filter, @route, fn f -> + .field + = text_input f, :name, class: "input input--wide", placeholder: "Name" + .fieldlabel + ' This is a friendly name for this filter - it should be short and descriptive. + .field + = textarea f, :description, class: "input input--wide", placeholder: "Description" + .fieldlabel + ' Here you can describe your filter in a bit more detail. + + + .field + = label f, :spoilered_tag_list, "Spoilered Tags" + br + = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :spoilered_tag_list, type: "edit" + .fieldlabel + ' These tags will spoiler the images they're associated with. + .field + = label f, :spoilered_complex_str, "Complex Spoiler Filter" + br + = textarea f, :spoilered_complex_str, class: "input input--wide", autocapitalize: "none" + .fieldlabel + p + ' Use the search syntax here to specify an additional filter. + ' For multiple filters, separate with a newline (or use the OR operator). Search fields excepting + code<> faved_by + ' are supported. See the search syntax guide + ' for more information. + p + strong> WARNING: + ' This filter is applied along with your tag filters. Tag filters may spoiler images that you mean to filter more precisely here. Double-check to make sure they don't interfere. + + + .field + = label f, :hidden_tag_list, "Hidden Tags" + br + = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :hidden_tag_list, type: "edit" + .fieldlabel + ' These tags will hide images entirely from the site; if you go directly to an image, it will spoiler it. + .field + = label f, :hidden_complex_str, "Complex Hide Filter" + br + = textarea f, :hidden_complex_str, class: "input input--wide", autocapitalize: "none" + .fieldlabel + p + ' Use the search syntax here to specify an additional filter. + ' For multiple filters, separate with a newline (or use the OR operator). Search fields excepting + code<> faved_by + ' are supported. See the search syntax guide + ' for more information. + p + strong> WARNING: + ' This filter is applied along with your tag filters. Tag filters may hide images that you mean to filter more precisely here. Double-check to make sure they don't interfere. + + = submit "Save Filter", class: "button" \ No newline at end of file diff --git a/lib/philomena_web/templates/filter/edit.html.slime b/lib/philomena_web/templates/filter/edit.html.slime new file mode 100644 index 00000000..14a90897 --- /dev/null +++ b/lib/philomena_web/templates/filter/edit.html.slime @@ -0,0 +1,2 @@ +h2 Editing Filter += render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :update, @filter) \ No newline at end of file diff --git a/lib/philomena_web/templates/filter/index.html.slime b/lib/philomena_web/templates/filter/index.html.slime index f6b1171c..4d0f4f34 100644 --- a/lib/philomena_web/templates/filter/index.html.slime +++ b/lib/philomena_web/templates/filter/index.html.slime @@ -26,11 +26,10 @@ h2 My Filters = if @current_user do - /- if can? :create, Filter - / p - / => link_to 'Click here to make a new filter from scratch', new_filter_path - / | or click "Copy and Customize" on a global or shared filter to use as a starting point. - /= render partial: 'filter_list', locals: { filters: @user_filters } + p + = link("Click here to make a new filter from scratch", to: Routes.filter_path(@conn, :new)) + = for filter <- @my_filters do + = render PhilomenaWeb.FilterView, "_filter.html", conn: @conn, filter: filter - else p ' If you're logged in, you can create and maintain custom filters here. diff --git a/lib/philomena_web/templates/filter/new.html.slime b/lib/philomena_web/templates/filter/new.html.slime new file mode 100644 index 00000000..de881073 --- /dev/null +++ b/lib/philomena_web/templates/filter/new.html.slime @@ -0,0 +1,2 @@ +h2 Creating New Filter += render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :create) \ No newline at end of file diff --git a/lib/philomena_web/templates/tag/_tag_editor.html.slime b/lib/philomena_web/templates/tag/_tag_editor.html.slime new file mode 100644 index 00000000..8ea139d0 --- /dev/null +++ b/lib/philomena_web/templates/tag/_tag_editor.html.slime @@ -0,0 +1,9 @@ +.js-tag-block class="fancy-tag-#{@type}" + = textarea @f, @name, class: "input input--wide tagsinput js-image-input js-taginput js-taginput-plain js-taginput-#{@name}", placeholder: "Add tags separated with commas" + .js-taginput.input.input--wide.tagsinput.hidden class="js-taginput-fancy" data-click-focus=".js-taginput-input.js-taginput-#{@name}" + input.input class="js-taginput-input js-taginput-#{@name}" id="taginput-fancy-#{@name}" type="text" placeholder="add a tag" autocapitalize="none" data-ac="true" data-ac-min-length="3" data-ac-source='/tags/autocomplete.json?term=' +button.button.button--state-primary.button--bold class="js-taginput-show" data-click-show=".js-taginput-fancy,.js-taginput-hide" data-click-hide=".js-taginput-plain,.js-taginput-show" data-click-focus=".js-taginput-input.js-taginput-#{@name}" + = hidden_input :fuck_ie, :fuck_ie, value: "fuck_ie" + ' Fancy Editor +button.hidden.button.button--state-primary.button--bold class="js-taginput-hide" data-click-show=".js-taginput-plain,.js-taginput-show" data-click-hide=".js-taginput-fancy,.js-taginput-hide" data-click-focus=".js-taginput-plain.js-taginput-#{@name}" + ' Plain Editor diff --git a/lib/pow_multi_factor/phoenix/controller_callbacks.ex b/lib/pow_multi_factor/phoenix/controller_callbacks.ex index f92658e9..1307d17d 100644 --- a/lib/pow_multi_factor/phoenix/controller_callbacks.ex +++ b/lib/pow_multi_factor/phoenix/controller_callbacks.ex @@ -25,29 +25,29 @@ defmodule PowMultiFactor.Phoenix.ControllerCallbacks do alias Pow.Plug alias PowMultiFactor.Plug, as: PowMultiFactorPlug - def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, _config) do + def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, config) do return_path = routes(conn).session_path(conn, :new) - clear_unauthorized(conn, {:ok, conn}, return_path) + clear_unauthorized(conn, config, {:ok, conn}, return_path) end - def before_respond(Pow.Phoenix.RegistrationController, :update, {:ok, user, conn}, _config) do + def before_respond(Pow.Phoenix.RegistrationController, :update, {:ok, user, conn}, config) do return_path = routes(conn).registration_path(conn, :edit) - halt_unauthorized(conn, {:ok, user, conn}, return_path) + halt_unauthorized(conn, config, {:ok, user, conn}, return_path) end - defp clear_unauthorized(conn, success_response, return_path) do - case PowMultiFactorPlug.mfa_unauthorized?(conn) do - true -> clear_auth(conn) |> go_back(return_path) - false -> success_response + defp clear_unauthorized(conn, config, success_response, return_path) do + case PowMultiFactorPlug.mfa_authorized?(conn, config) do + false -> clear_auth(conn) |> go_back(return_path) + true -> success_response end end - defp halt_unauthorized(conn, success_response, return_path) do - case PowMultiFactorPlug.mfa_unauthorized?(conn) do - true -> go_back(conn, return_path) - false -> success_response + defp halt_unauthorized(conn, config, success_response, return_path) do + case PowMultiFactorPlug.mfa_authorized?(conn, config) do + false -> go_back(conn, return_path) + true -> success_response end end diff --git a/lib/pow_multi_factor/plug.ex b/lib/pow_multi_factor/plug.ex index e5b0eeec..33526153 100644 --- a/lib/pow_multi_factor/plug.ex +++ b/lib/pow_multi_factor/plug.ex @@ -3,24 +3,35 @@ defmodule PowMultiFactor.Plug do Plug helper methods. """ + alias Plug.Crypto alias Pow.Plug - #alias PowMultiFactor.Ecto.Context + alias Pow.Config - def mfa_unauthorized?(conn) do + def mfa_authorized?(conn, config) do user = Plug.current_user(conn) if user.otp_required_for_login do - true + secret = user.__struct__.otp_secret(user) + totp = Elixir2fa.generate_totp(secret) + + Crypto.secure_compare(totp, conn.params) else - false + true end end - #defp otp_secret(user) do + def assign_mfa(conn, config) do + user = Plug.current_user(conn) + repo = Config.repo!(config) - #end + if user.encrypted_otp_secret in [nil, ""] do + {:ok, user} = + user.__struct__.put_otp_secret(Elixir2fa.random_secret()) + |> repo.update() - #defp otp_shared_key do - # Application.get_env - #end + user + else + user + end + end end