Remove the /ip_addresses page. This page allowed moderators to search users by IP, and to see recent activity tied to an IP. However, it was limited to IPs tied to uploads, comments, dmails, artist edits, note edits, and wiki edits. Remove this page because it was limited in scope and because there are better ways of doing what it did. The /user_events page is better at catching sockpuppets because it tracks IPs for every login, not just for certain types of edits. And the /user_actions page is better at monitoring user activity because it shows all activity associated with an account, not just for certain types of edits. Removing this allows us to drop IP addresses from all tables besides the user_events table. This is good because these IPs are no longer necessary for any purpose, and because storing them forever is a liability.
445 lines
13 KiB
Ruby
445 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module ApplicationHelper
|
|
USER_DATA_ATTRIBUTES = %i[
|
|
id name level level_string theme comment_threshold default_image_size time_zone per_page
|
|
] + User::ACTIVE_BOOLEAN_ATTRIBUTES + User::Roles.map { |role| :"is_#{role}?" }
|
|
|
|
COOKIE_DATA_ATTRIBUTES = %i[
|
|
news-ticker hide_upgrade_account_notice hide_verify_account_notice
|
|
hide_dmail_notice dab show-relationship-previews post_preview_size
|
|
post_preview_show_votes
|
|
]
|
|
|
|
def listing_type(*fields, member_check: true, types: [:revert, :standard])
|
|
(fields.reduce(false) { |acc, field| acc || params.dig(:search, field).present? } && (!member_check || CurrentUser.is_member?) ? types[0] : types[1])
|
|
end
|
|
|
|
def diff_list_html(this_list, other_list, ul_class: ["diff-list"], li_class: [], show_unchanged: true)
|
|
diff = SetDiff.new(this_list, other_list)
|
|
render "diff_list", diff: diff, ul_class: ul_class, li_class: li_class, show_unchanged: show_unchanged
|
|
end
|
|
|
|
def diff_name_html(this_name, other_name)
|
|
DiffBuilder.new(this_name, other_name, /./).build
|
|
end
|
|
|
|
def diff_body_html(record, other, field)
|
|
if record.blank? || other.blank?
|
|
diff_record = other.presence || record
|
|
return h(diff_record[field]).gsub(/\r?\n/, '<span class="paragraph-mark">¶</span><br>').html_safe
|
|
end
|
|
|
|
pattern = /(?:<.+?>)|(?:\w+)|(?:[ \t]+)|(?:\r?\n)|(?:.+?)/
|
|
DiffBuilder.new(record[field], other[field], pattern).build
|
|
end
|
|
|
|
def status_diff_html(record, type)
|
|
other = record.send(type)
|
|
|
|
if other.blank?
|
|
return type == "previous" ? "New" : ""
|
|
end
|
|
|
|
changed_fields = record.class.status_fields.select do |field, _status|
|
|
(record.has_attribute?(field) && record[field] != other[field]) ||
|
|
(!record.has_attribute?(field) && record.send(field, type))
|
|
end
|
|
|
|
statuses = changed_fields.map { |field, status| status }
|
|
altered = record.updater_id != other.updater_id
|
|
|
|
tag.div(class: "version-statuses", "data-altered": altered) do
|
|
safe_join(statuses, tag.br)
|
|
end
|
|
end
|
|
|
|
def wordbreakify(string)
|
|
lines = string.scan(/.{1,10}/)
|
|
wordbreaked_string = lines.map {|str| h(str)}.join("<wbr>")
|
|
raw(wordbreaked_string)
|
|
end
|
|
|
|
def version_type_links(params)
|
|
html = []
|
|
%w[previous subsequent current].each do |type|
|
|
if type == params[:type]
|
|
html << %{<span>#{type}</span>}
|
|
else
|
|
html << tag.li(link_to(type, params.except(:controller, :action).merge(type: type).permit!))
|
|
end
|
|
end
|
|
html.join(" | ").html_safe
|
|
end
|
|
|
|
def current_page_path(**params)
|
|
url_for(request.query_parameters.merge(params))
|
|
end
|
|
|
|
def nav_link_to(text, url, **options)
|
|
klass = options.delete(:class)
|
|
|
|
if nav_link_match(params[:controller], url)
|
|
klass = "#{klass} current"
|
|
end
|
|
|
|
li_link_to(text, url, id_prefix: "nav-", class: klass, **options)
|
|
end
|
|
|
|
def subnav_link_to(text, url, **options)
|
|
li_link_to(text, url, id_prefix: "subnav-", **options)
|
|
end
|
|
|
|
def li_link_to(text, url, id_prefix: "", **options)
|
|
klass = options.delete(:class)
|
|
id = id_prefix + text.downcase.gsub(/[^a-z ]/, "").parameterize
|
|
tag.li(link_to(text, url, id: "#{id}-link", **options), id: id, class: klass)
|
|
end
|
|
|
|
def format_text(text, **options)
|
|
raw DText.format_text(text, **options)
|
|
end
|
|
|
|
def strip_dtext(text)
|
|
DText.strip_dtext(text)
|
|
end
|
|
|
|
def time_tag(content, time, **options)
|
|
datetime = time.strftime("%Y-%m-%dT%H:%M%:z")
|
|
|
|
tag.time content || datetime, datetime: datetime, title: time.to_formatted_s, **options
|
|
end
|
|
|
|
def humanized_duration(duration)
|
|
if duration >= 100.years
|
|
"forever"
|
|
else
|
|
duration.inspect
|
|
end
|
|
end
|
|
|
|
def duration_to_hhmmss(seconds)
|
|
seconds = seconds.round
|
|
hh = seconds.div(1.hour).to_s
|
|
mm = seconds.div(1.minute).to_s
|
|
ss = "%.2d" % (seconds % 1.minute)
|
|
|
|
if seconds >= 1.hour
|
|
"#{hh}:#{mm}:#{ss}"
|
|
elsif seconds >= 1.second
|
|
"#{mm}:#{ss}"
|
|
else
|
|
"0:01"
|
|
end
|
|
end
|
|
|
|
def humanized_number(number, million: "M", thousand: "k")
|
|
if number >= 1_000_000
|
|
format("%.1f#{million}", number / 1_000_000.0)
|
|
elsif number >= 10_000
|
|
"#{number / 1_000}#{thousand}"
|
|
elsif number >= 1_000
|
|
format("%.1f#{thousand}", number / 1_000.0)
|
|
else
|
|
number.to_s
|
|
end
|
|
end
|
|
|
|
def time_ago_in_words_tagged(time, compact: false)
|
|
if time.nil?
|
|
tag.em(tag.time("unknown"))
|
|
elsif time.past?
|
|
if compact
|
|
text = time_ago_in_words(time)
|
|
text = text.gsub(/almost|about|over/, "").strip
|
|
text = text.gsub(/less than a/, "<1")
|
|
text = text.gsub(/ minutes?/, "m")
|
|
text = text.gsub(/ hours?/, "h")
|
|
text = text.gsub(/ days?/, "d")
|
|
text = text.gsub(/ months?/, "mo")
|
|
text = text.gsub(/ years?/, "y")
|
|
klass = "compact-timestamp"
|
|
else
|
|
text = time_ago_in_words(time) + " ago"
|
|
klass = ""
|
|
end
|
|
|
|
time_tag(text, time, class: klass)
|
|
elsif time.future?
|
|
if compact
|
|
text = distance_of_time_in_words(Time.now, time)
|
|
text = text.gsub(/almost|about|over/, "").gsub(/less than a/, "<1").strip
|
|
time_tag(text, time)
|
|
else
|
|
time_tag("in " + distance_of_time_in_words(Time.now, time), time)
|
|
end
|
|
end
|
|
end
|
|
|
|
def compact_time(time)
|
|
time_tag(time.strftime("%Y-%m-%d %H:%M"), time)
|
|
end
|
|
|
|
def external_link_to(url, text = url, truncate: nil, strip: false, **link_options)
|
|
text = text.gsub(%r{\Ahttps?://}i, "") if strip == :scheme
|
|
text = text.gsub(%r{\Ahttps?://(?:www\.)?}i, "") if strip == :subdomain
|
|
text = text.truncate(truncate) if truncate
|
|
|
|
if url =~ %r{\Ahttps?://}i
|
|
link_to text, url, rel: "external noreferrer nofollow", **link_options
|
|
else
|
|
url
|
|
end
|
|
end
|
|
|
|
def link_to_ip(ip, shorten: false, **options)
|
|
ip_addr = IPAddr.new(ip.to_s)
|
|
ip_addr.prefix = 64 if ip_addr.ipv6? && shorten
|
|
link_to ip_addr.to_s, user_events_path(search: { user_session: { ip_addr: ip }}), **options
|
|
end
|
|
|
|
def link_to_search(tag, **options)
|
|
link_to tag.pretty_name, posts_path(tags: tag.name), class: tag_class(tag), **options
|
|
end
|
|
|
|
def link_to_wiki(text, title = text, **options)
|
|
title = "~#{title}" if title =~ /\A\d+\z/
|
|
link_to text, wiki_page_path(title), class: "wiki-link", **options
|
|
end
|
|
|
|
def link_to_wikis(*wiki_titles, **options)
|
|
links = wiki_titles.map do |title|
|
|
link_to_wiki title.tr("_", " "), title
|
|
end
|
|
|
|
to_sentence(links, **options)
|
|
end
|
|
|
|
def link_to_user(user, text = nil, classes: nil, **options)
|
|
return "anonymous" if user.blank?
|
|
|
|
user_class = "user user-#{user.level_string.downcase} #{classes}"
|
|
user_class += " user-post-approver" if user.can_approve_posts?
|
|
user_class += " user-post-uploader" if user.can_upload_free?
|
|
user_class += " user-banned" if user.is_banned?
|
|
|
|
text = user.pretty_name if text.blank?
|
|
data = { "user-id": user.id, "user-name": user.name, "user-level": user.level }
|
|
link_to(text, user, class: user_class, data: data)
|
|
end
|
|
|
|
def embed_wiki(title, **options)
|
|
wiki = WikiPage.find_by(title: title)
|
|
text = format_text(wiki&.body)
|
|
tag.div(text, class: "prose", **options)
|
|
end
|
|
|
|
def dtext_preview_button(preview_field)
|
|
tag.input value: "Preview", type: "button", class: "dtext-preview-button", "data-preview-field": preview_field
|
|
end
|
|
|
|
def quick_search_form_for(attribute, url, name, autocomplete: nil, redirect: false, &block)
|
|
tag.li do
|
|
search_form_for(url, classes: "quick-search-form one-line-form") do |f|
|
|
out = f.input attribute, label: false, placeholder: "Search #{name}", input_html: { id: nil, "data-autocomplete": autocomplete }
|
|
out += tag.input type: :hidden, name: :redirect, value: redirect
|
|
out += capture { yield f } if block_given?
|
|
out
|
|
end
|
|
end
|
|
end
|
|
|
|
def search_form_for(url, classes: "inline-form", method: :get, &block)
|
|
defaults = { required: false }
|
|
html_options = { autocomplete: "off", class: "search-form #{classes}" }
|
|
|
|
simple_form_for(:search, method: method, url: url, defaults: defaults, html: html_options) do |f|
|
|
out = "".html_safe
|
|
out += tag.input(type: :hidden, name: :limit, value: params[:limit]) if params[:limit].present?
|
|
out += capture { yield f } if block_given?
|
|
out
|
|
end
|
|
end
|
|
|
|
def edit_form_for(model, **options, &block)
|
|
options[:html] = { autocomplete: "off", **options[:html].to_h }
|
|
options[:authenticity_token] = true if options[:remote] == true
|
|
|
|
simple_form_for(model, **options) do |form|
|
|
if model.try(:errors).try(:any?)
|
|
concat tag.div(format_text(model.errors.full_messages.join("; ")), class: "notice notice-error notice-small prose")
|
|
end
|
|
|
|
if model.try(:warnings).try(:any?)
|
|
concat tag.div(format_text(model.warnings.full_messages.join("; ")), class: "notice notice-info notice-small prose")
|
|
end
|
|
|
|
block.call(form)
|
|
end
|
|
end
|
|
|
|
def table_for(...)
|
|
table = TableBuilder.new(...)
|
|
render "table_builder/table", table: table
|
|
end
|
|
|
|
def body_attributes(current_user, params, current_item, exception)
|
|
if exception
|
|
controller_param = "static"
|
|
action_param = "error"
|
|
layout = nil
|
|
extra_attributes = {}
|
|
else
|
|
controller_param = params[:controller].parameterize.dasherize
|
|
action_param = params[:action].parameterize.dasherize
|
|
layout = controller.class.send(:_layout)
|
|
extra_attributes = current_item_data_attributes(current_item)
|
|
end
|
|
|
|
{
|
|
lang: "en",
|
|
class: "c-#{controller_param} a-#{action_param} flex flex-col",
|
|
spellcheck: "false",
|
|
data: {
|
|
controller: controller_param,
|
|
action: action_param,
|
|
layout: layout,
|
|
"current-user-ip-addr": request.remote_ip,
|
|
"current-user-save-data": CurrentUser.save_data,
|
|
**data_attributes_for(current_user, "current-user", USER_DATA_ATTRIBUTES),
|
|
**data_attributes_for(cookies, "cookie", COOKIE_DATA_ATTRIBUTES),
|
|
**extra_attributes,
|
|
}
|
|
}
|
|
end
|
|
|
|
def current_item_data_attributes(current_item)
|
|
if current_item.present? && current_item.respond_to?(:html_data_attributes) && current_item.respond_to?(:model_name)
|
|
model_name = current_item.model_name.singular.dasherize
|
|
model_attributes = current_item.html_data_attributes
|
|
data_attributes_for(current_item, model_name, model_attributes)
|
|
else
|
|
{}
|
|
end
|
|
end
|
|
|
|
def data_attributes_for(record, prefix = "data", attributes = record.html_data_attributes)
|
|
attributes.map do |attr|
|
|
if attr.is_a?(Array)
|
|
name = attr.map {|sym| sym.to_s.dasherize.delete("?")}.join('-')
|
|
value = record
|
|
attr.each do |sym|
|
|
value = value.send(sym)
|
|
if value.nil?
|
|
break
|
|
end
|
|
end
|
|
elsif record.respond_to?(attr)
|
|
name = attr.to_s.dasherize.delete("?")
|
|
value = record.send(attr)
|
|
else
|
|
name = attr.to_s.dasherize.delete("?")
|
|
value = record[attr]
|
|
end
|
|
|
|
if value.nil?
|
|
value = "null"
|
|
end
|
|
|
|
if prefix.blank?
|
|
[:"#{name}", value]
|
|
else
|
|
[:"#{prefix}-#{name}", value]
|
|
end
|
|
end.to_h
|
|
end
|
|
|
|
def page_title(title = nil, suffix: "| #{Danbooru.config.app_name}")
|
|
if title.present?
|
|
content_for(:page_title) { "#{title} #{suffix}".strip }
|
|
elsif content_for(:page_title).present?
|
|
content_for(:page_title)
|
|
elsif params[:action] == "index"
|
|
"#{params[:controller].titleize} #{suffix}"
|
|
elsif params[:action] == "show"
|
|
"#{params[:controller].singularize.titleize} #{suffix}"
|
|
elsif params[:action] == "new"
|
|
"New #{params[:controller].singularize.titleize} #{suffix}"
|
|
elsif params[:action] == "edit"
|
|
"Edit #{params[:controller].singularize.titleize} #{suffix}"
|
|
elsif params[:action] == "search"
|
|
"Search #{params[:controller].titleize} #{suffix}"
|
|
else
|
|
"#{Danbooru.config.app_name}/#{params[:controller]}"
|
|
end
|
|
end
|
|
|
|
def meta_description(description = nil)
|
|
if description.present?
|
|
content_for(:meta_description) { description }
|
|
elsif content_for(:meta_description).present?
|
|
content_for(:meta_description)
|
|
end
|
|
end
|
|
|
|
def canonical_url(url = nil)
|
|
if url.present?
|
|
content_for(:canonical_url) { url }
|
|
elsif content_for(:canonical_url).present?
|
|
content_for(:canonical_url)
|
|
else
|
|
request_params = request.params.sort.to_h.with_indifferent_access
|
|
request_params.delete(:page) if request_params[:page].to_i == 1
|
|
request_params.delete(:limit)
|
|
url_for(**request_params, host: Danbooru.config.hostname, only_path: false)
|
|
end
|
|
end
|
|
|
|
def noindex
|
|
content_for(:html_header, tag.meta(name: "robots", content: "noindex"))
|
|
end
|
|
|
|
def atom_feed_tag(title, url = {})
|
|
content_for(:html_header, auto_discovery_link_tag(:atom, url, title: title))
|
|
end
|
|
|
|
protected
|
|
|
|
def nav_link_match(controller, url)
|
|
url =~ case controller
|
|
when "sessions", "users", "admin/users"
|
|
%r{^/(session|users)}
|
|
|
|
when "comments"
|
|
%r{^/comments}
|
|
|
|
when "notes", "note_versions"
|
|
%r{^/notes}
|
|
|
|
when "posts", "uploads", "post_versions", "explore/posts", "moderator/post/dashboards", "favorites"
|
|
%r{^/post}
|
|
|
|
when "artists", "artist_versions"
|
|
%r{^/artist}
|
|
|
|
when "tags", "tag_aliases", "tag_implications"
|
|
%r{^/tags}
|
|
|
|
when "pools", "pool_versions"
|
|
%r{^/pools}
|
|
|
|
when "moderator/dashboards"
|
|
%r{^/moderator}
|
|
|
|
when "wiki_pages", "wiki_page_versions"
|
|
%r{^/wiki_pages}
|
|
|
|
when "forum_topics", "forum_posts"
|
|
%r{^/forum_topics}
|
|
|
|
else
|
|
%r{^/static}
|
|
end
|
|
end
|
|
end
|