defmodule PlausibleWeb.Live.Shields.PageRules do @moduledoc """ LiveView allowing page Rules management """ use PlausibleWeb, :live_component alias PlausibleWeb.Live.Components.Modal alias Plausible.Shields alias Plausible.Shield alias Plausible.Stats.QueryBuilder def update(assigns, socket) do socket = socket |> assign( page_rules_count: assigns[:page_rules_count] || socket.assigns.page_rules_count, site: assigns[:site] || socket.assigns.site, current_user: assigns[:current_user] || socket.assigns.current_user, form: new_form() ) |> assign_new(:page_rules, fn %{site: site} -> Shields.list_page_rules(site) end) |> assign_new(:redundant_rules, fn %{page_rules: page_rules} -> detect_redundancy(page_rules) end) {:ok, socket} end def render(assigns) do ~H"""
<.settings_tiles> <.tile docs="top-pages#block-traffic-from-specific-pages-or-sections"> <:title>Pages block list <:subtitle :if={not Enum.empty?(@page_rules)}> Reject incoming traffic for specific pages. <%= if Enum.empty?(@page_rules) do %>

Block a page

Reject incoming traffic for specific pages. <.styled_link href="https://plausible.io/docs/top-pages#block-traffic-from-specific-pages-or-sections" target="_blank" > Learn more

<.button :if={@page_rules_count < Shields.maximum_page_rules()} id="add-page-rule" x-data x-on:click={Modal.JS.open("page-rule-form-modal")} class="mt-4" > Add page
<% else %> <.filter_bar :if={@page_rules_count < Shields.maximum_page_rules()} filtering_enabled?={false} > <.button id="add-page-rule" x-data x-on:click={Modal.JS.open("page-rule-form-modal")} mt?={false} > Add page <.notice :if={@page_rules_count >= Shields.maximum_page_rules()} class="mt-4" title="Maximum number of pages reached" theme={:gray} >

You've reached the maximum number of pages you can block ({Shields.maximum_page_rules()}). Please remove one before adding another.

<.table rows={@page_rules}> <:thead> <.th>Page <.th hide_on_mobile>Status <.th invisible>Actions <:tbody :let={rule}> <.td max_width="max-w-40" truncate> {rule.page_path} <.td hide_on_mobile>
Blocked Allowed
<.td actions> <.delete_button id={"remove-page-rule-#{rule.id}"} phx-target={@myself} phx-click="remove-page-rule" phx-value-rule-id={rule.id} data-confirm="Are you sure you want to revoke this rule?" />
<% end %> <.live_component :let={modal_unique_id} module={Modal} id="page-rule-form-modal"> <.form :let={f} for={@form} phx-submit="save-page-rule" phx-target={@myself} class="max-w-md w-full mx-auto" > <.title>Add page to block list <.live_component class="mt-4" submit_name="page_rule[page_path]" submit_value={f[:page_path].value} display_value={f[:page_path].value || ""} module={PlausibleWeb.Live.Components.ComboBox} suggest_fun={ fn input, options -> suggest_page_paths(input, options, @site, @page_rules) end } id={"#{f[:page_path].id}-#{modal_unique_id}"} creatable /> <.error :for={msg <- f[:page_path].errors}>{translate_error(msg)}

You can use a wildcard (*) to match multiple pages. For example, /blog/* will match /blog/post. Once added, we will start rejecting traffic from this page within a few minutes.

<.button type="submit" class="w-full"> Add page
""" end def handle_event("save-page-rule", %{"page_rule" => params}, socket) do user = socket.assigns.current_user case Shields.add_page_rule( socket.assigns.site.id, params, added_by: user ) do {:ok, rule} -> page_rules = [rule | socket.assigns.page_rules] socket = socket |> Modal.close("page-rule-form-modal") |> assign( form: new_form(), page_rules: page_rules, page_rules_count: socket.assigns.page_rules_count + 1, redundant_rules: detect_redundancy(page_rules) ) send_flash( :success, "Page rule added successfully. Traffic will be rejected within a few minutes." ) {:noreply, socket} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end end def handle_event("remove-page-rule", %{"rule-id" => rule_id}, socket) do Shields.remove_page_rule(socket.assigns.site.id, rule_id) send_flash( :success, "Page rule removed successfully. Traffic will be resumed within a few minutes." ) page_rules = Enum.reject(socket.assigns.page_rules, &(&1.id == rule_id)) {:noreply, socket |> assign( page_rules_count: socket.assigns.page_rules_count - 1, page_rules: page_rules, redundant_rules: detect_redundancy(page_rules) )} end def send_flash(kind, message) do send(self(), {:flash, kind, message}) end defp new_form() do %Shield.PageRule{} |> Shield.PageRule.changeset(%{}) |> to_form() end defp format_added_at(dt, tz) do dt |> Plausible.Timezones.to_datetime_in_timezone(tz) |> Calendar.strftime("%Y-%m-%d %H:%M:%S") end def suggest_page_paths(input, _options, site, page_rules) do query = QueryBuilder.build!(site, input_date_range: :all, metrics: [:pageviews], filters: [[:is_not, "event:page", Enum.map(page_rules, & &1.page_path)]] ) site |> Plausible.Stats.filter_suggestions(query, "page", input) |> Enum.map(fn %{label: label, value: value} -> {label, value} end) end defp detect_redundancy(page_rules) do page_rules |> Enum.reduce(%{}, fn rule, acc -> {[^rule], remaining_rules} = Enum.split_with( page_rules, fn r -> r == rule end ) conflicting = remaining_rules |> Enum.filter(fn candidate -> rule |> Map.fetch!(:page_path_pattern) |> maybe_compile() |> Regex.match?(candidate.page_path) end) |> Enum.map(& &1.id) Enum.reduce(conflicting, acc, fn conflicting_rule_id, acc -> Map.update(acc, conflicting_rule_id, [rule.page_path], fn existing -> [rule.page_path | existing] end) end) end) end defp maybe_compile(pattern) when is_binary(pattern), do: Regex.compile!(pattern) defp maybe_compile(%Regex{} = pattern), do: pattern end