Merge branch 'develop' into issue/1934-welcome-email
This commit is contained in:
commit
67ab9a7928
47 changed files with 579 additions and 174 deletions
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
@spec verify!() :: :ok | VerifyError.t()
|
||||
def verify! do
|
||||
:ok
|
||||
|> check_confirmation_accounts!
|
||||
|> check_migrations_applied!()
|
||||
|> check_welcome_message_config!()
|
||||
|> check_rum!()
|
||||
|
|
@ -41,6 +42,24 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
|
||||
defp check_welcome_message_config!(result), do: result
|
||||
|
||||
# Checks account confirmation email
|
||||
#
|
||||
def check_confirmation_accounts!(:ok) do
|
||||
if Pleroma.Config.get([:instance, :account_activation_required]) &&
|
||||
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
|
||||
Logger.error(
|
||||
"Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
|
||||
)
|
||||
|
||||
{:error,
|
||||
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_confirmation_accounts!(result), do: result
|
||||
|
||||
# Checks for pending migrations.
|
||||
#
|
||||
def check_migrations_applied!(:ok) do
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ defmodule Pleroma.ConfigDB do
|
|||
{:quack, :meta},
|
||||
{:mime, :types},
|
||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||
{:auto_linker, :opts},
|
||||
{:swarm, :node_blacklist},
|
||||
{:logger, :backends}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,11 +10,15 @@ defmodule Pleroma.Formatter do
|
|||
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||
|
||||
@auto_linker_config hashtag: true,
|
||||
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
||||
mention: true,
|
||||
mention_handler: &Pleroma.Formatter.mention_handler/4,
|
||||
scheme: true
|
||||
defp linkify_opts do
|
||||
Pleroma.Config.get(Pleroma.Formatter) ++
|
||||
[
|
||||
hashtag: true,
|
||||
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
||||
mention: true,
|
||||
mention_handler: &Pleroma.Formatter.mention_handler/4
|
||||
]
|
||||
end
|
||||
|
||||
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
||||
case User.get_cached_by_nickname(nickname) do
|
||||
|
|
@ -80,19 +84,19 @@ defmodule Pleroma.Formatter do
|
|||
@spec linkify(String.t(), keyword()) ::
|
||||
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
|
||||
def linkify(text, options \\ []) do
|
||||
options = options ++ @auto_linker_config
|
||||
options = linkify_opts() ++ options
|
||||
|
||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||
|
||||
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
|
||||
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
|
||||
{text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
|
||||
{text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
|
||||
|
||||
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||
else
|
||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
|
||||
{text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
|
||||
|
||||
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||
end
|
||||
|
|
@ -111,9 +115,9 @@ defmodule Pleroma.Formatter do
|
|||
|
||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
|
||||
Linkify.link(mentions, options) <> Linkify.link(rest, options)
|
||||
else
|
||||
AutoLinker.link(text, options)
|
||||
Linkify.link(text, options)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -96,16 +96,18 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|
|||
|
||||
def response("/main/public") do
|
||||
posts =
|
||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"], "local_only" => true})
|
||||
|> render_activities
|
||||
%{type: ["Create"], local_only: true}
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
|
||||
end
|
||||
|
||||
def response("/main/all") do
|
||||
posts =
|
||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"]})
|
||||
|> render_activities
|
||||
%{type: ["Create"]}
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
|
||||
end
|
||||
|
|
@ -130,13 +132,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
|
|||
def response("/users/" <> nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
params = %{
|
||||
"type" => ["Create"],
|
||||
"actor_id" => user.ap_id
|
||||
type: ["Create"],
|
||||
actor_id: user.ap_id
|
||||
}
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_public_activities(params)
|
||||
|> render_activities
|
||||
params
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> render_activities()
|
||||
|
||||
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
|
||||
else
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:error, "Object has been deleted"} ->
|
||||
nil
|
||||
|
||||
{:reject, reason} ->
|
||||
Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
|
||||
nil
|
||||
|
||||
e ->
|
||||
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||
nil
|
||||
|
|
|
|||
|
|
@ -740,21 +740,25 @@ defmodule Pleroma.User do
|
|||
|
||||
def send_welcome_email(_), do: {:ok, :noop}
|
||||
|
||||
def try_send_confirmation_email(%User{} = user) do
|
||||
if user.confirmation_pending &&
|
||||
Config.get([:instance, :account_activation_required]) do
|
||||
user
|
||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
|
||||
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
|
||||
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
send_confirmation_email(user)
|
||||
{:ok, :enqueued}
|
||||
else
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
def try_send_confirmation_email(users) do
|
||||
Enum.each(users, &try_send_confirmation_email/1)
|
||||
def try_send_confirmation_email(_), do: {:ok, :noop}
|
||||
|
||||
@spec send_confirmation_email(Uset.t()) :: User.t()
|
||||
def send_confirmation_email(%User{} = user) do
|
||||
user
|
||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def needs_update?(%User{local: true}), do: false
|
||||
|
|
|
|||
|
|
@ -1370,6 +1370,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, {:reject, reason} = e} ->
|
||||
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.drop(["conversation"])
|
||||
else
|
||||
e ->
|
||||
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
else
|
||||
|
|
|
|||
|
|
@ -719,15 +719,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
case Activity.get_by_ap_id_with_object(id) do
|
||||
%Activity{} = activity ->
|
||||
activity_actor = User.get_by_ap_id(activity.object.data["actor"])
|
||||
|
||||
%{
|
||||
"type" => "Note",
|
||||
"id" => activity.data["id"],
|
||||
"content" => activity.object.data["content"],
|
||||
"published" => activity.object.data["published"],
|
||||
"actor" =>
|
||||
AccountView.render("show.json", %{
|
||||
user: User.get_by_ap_id(activity.object.data["actor"])
|
||||
})
|
||||
AccountView.render(
|
||||
"show.json",
|
||||
%{user: activity_actor, skip_visibility_check: true}
|
||||
)
|
||||
}
|
||||
|
||||
_ ->
|
||||
|
|
|
|||
|
|
@ -345,7 +345,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
|
||||
json(
|
||||
conn,
|
||||
AccountView.render("index.json", users: users, count: count, page_size: page_size)
|
||||
AccountView.render("index.json",
|
||||
users: users,
|
||||
count: count,
|
||||
page_size: page_size
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -616,29 +620,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
end
|
||||
|
||||
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
|
||||
User.toggle_confirmation(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "confirm_email"
|
||||
})
|
||||
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
|
||||
|
||||
json(conn, "")
|
||||
end
|
||||
|
||||
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
users =
|
||||
Enum.map(nicknames, fn nickname ->
|
||||
nickname
|
||||
|> User.get_cached_by_nickname()
|
||||
|> User.send_confirmation_email()
|
||||
end)
|
||||
|
||||
User.try_send_confirmation_email(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "resend_confirmation_email"
|
||||
})
|
||||
ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
|
||||
|
||||
json(conn, "")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
|||
end
|
||||
|
||||
def merge_account_views(%User{} = user) do
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
defmodule Pleroma.Web.ChatChannel do
|
||||
use Phoenix.Channel
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ChatChannel.ChatChannelState
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
|
||||
def join("chat:public", _message, socket) do
|
||||
send(self(), :after_join)
|
||||
|
|
@ -22,9 +24,9 @@ defmodule Pleroma.Web.ChatChannel do
|
|||
|
||||
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
|
||||
author = User.get_cached_by_nickname(user_name)
|
||||
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
|
||||
author_json = AccountView.render("show.json", user: author, skip_visibility_check: true)
|
||||
|
||||
message = ChatChannelState.add_message(%{text: text, author: author})
|
||||
message = ChatChannelState.add_message(%{text: text, author: author_json})
|
||||
|
||||
broadcast!(socket, "new_msg", message)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
|||
AccountView.render("index.json",
|
||||
users: accounts,
|
||||
for: options[:for_user],
|
||||
as: :user,
|
||||
embed_relationships: options[:embed_relationships]
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,21 +27,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
UserRelationship.view_relationships_option(reading_user, users)
|
||||
end
|
||||
|
||||
opts = Map.put(opts, :relationships, relationships_opt)
|
||||
opts =
|
||||
opts
|
||||
|> Map.merge(%{relationships: relationships_opt, as: :user})
|
||||
|> Map.delete(:users)
|
||||
|
||||
users
|
||||
|> render_many(AccountView, "show.json", opts)
|
||||
|> Enum.filter(&Enum.any?/1)
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user} = opts) do
|
||||
if User.visible_for(user, opts[:for]) == :visible do
|
||||
@doc """
|
||||
Renders specified user account.
|
||||
:skip_visibility_check option skips visibility check and renders any user (local or remote)
|
||||
regardless of [:pleroma, :restrict_unauthenticated] setting.
|
||||
:for option specifies the requester and can be a User record or nil.
|
||||
Only use `user: user, for: user` when `user` is the actual requester of own profile.
|
||||
"""
|
||||
def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
|
||||
do_render("show.json", opts)
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
|
||||
if User.visible_for(user, for_user_or_nil) == :visible do
|
||||
do_render("show.json", opts)
|
||||
else
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
def render("show.json", _) do
|
||||
raise "In order to prevent account accessibility issues, " <>
|
||||
":skip_visibility_check or :for option is required."
|
||||
end
|
||||
|
||||
def render("mention.json", %{user: user}) do
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
|
|||
|
||||
%{
|
||||
id: participation.id |> to_string(),
|
||||
accounts: render(AccountView, "index.json", users: users, as: :user),
|
||||
accounts: render(AccountView, "index.json", users: users, for: user),
|
||||
unread: !participation.read,
|
||||
last_status:
|
||||
render(StatusView, "show.json",
|
||||
|
|
|
|||
|
|
@ -89,11 +89,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|
|||
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
||||
|> render("show.json", chat_message_reference: cm_ref)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
|
||||
def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{
|
||||
id: chat_id,
|
||||
message_id: message_id
|
||||
}) do
|
||||
|
|
@ -104,12 +104,15 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|
|||
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
||||
|> render("show.json", chat_message_reference: cm_ref)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_read(
|
||||
%{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
|
||||
%{
|
||||
body_params: %{last_read_id: last_read_id},
|
||||
assigns: %{user: %{id: user_id}}
|
||||
} = conn,
|
||||
%{id: id}
|
||||
) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
|
||||
|
|
@ -121,7 +124,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|
|||
end
|
||||
end
|
||||
|
||||
def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
|
||||
def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
|
||||
cm_refs =
|
||||
chat
|
||||
|
|
@ -130,7 +133,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|
|||
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("index.json", for: user, chat_message_references: cm_refs)
|
||||
|> render("index.json", chat_message_references: cm_refs)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|
|||
]
|
||||
)
|
||||
|
||||
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
|
||||
plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
|
||||
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||
plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
|
|||
def render("show.json", %{chat: %Chat{} = chat} = opts) do
|
||||
recipient = User.get_cached_by_ap_id(chat.recipient)
|
||||
last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
|
||||
account_view_opts = account_view_opts(opts, recipient)
|
||||
|
||||
%{
|
||||
id: chat.id |> to_string(),
|
||||
account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
|
||||
account: AccountView.render("show.json", account_view_opts),
|
||||
unread: MessageReference.unread_count_for_chat(chat),
|
||||
last_message:
|
||||
last_message &&
|
||||
|
|
@ -27,7 +28,17 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("index.json", %{chats: chats}) do
|
||||
render_many(chats, __MODULE__, "show.json")
|
||||
def render("index.json", %{chats: chats} = opts) do
|
||||
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
|
||||
end
|
||||
|
||||
defp account_view_opts(opts, recipient) do
|
||||
account_view_opts = Map.put(opts, :user, recipient)
|
||||
|
||||
if Map.has_key?(account_view_opts, :for) do
|
||||
account_view_opts
|
||||
else
|
||||
Map.put(account_view_opts, :skip_visibility_check, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
|
|||
%{
|
||||
name: emoji,
|
||||
count: length(users),
|
||||
accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
|
||||
accounts: render(AccountView, "index.json", users: users, for: user),
|
||||
me: !!(user && user.ap_id in user_ap_ids)
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
|||
|
||||
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
|
||||
defp validate_page_url(page_url) when is_binary(page_url) do
|
||||
validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
|
||||
validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld])
|
||||
|
||||
page_url
|
||||
|> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld)
|
||||
|> Linkify.Parser.url?(validate_tld: validate_tld)
|
||||
|> parse_uri(page_url)
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue