From 2ae912a8be672a117c809f7405f2ab94d8ca9759 Mon Sep 17 00:00:00 2001
From: BrokenEagle
Date: Sat, 14 Nov 2020 17:59:14 +0000
Subject: [PATCH 001/132] Add childlike font
---
app/javascript/src/styles/common/fonts.css | 9 +++++++++
public/fonts/gisele.ttf | Bin 0 -> 13356 bytes
2 files changed, 9 insertions(+)
create mode 100644 public/fonts/gisele.ttf
diff --git a/app/javascript/src/styles/common/fonts.css b/app/javascript/src/styles/common/fonts.css
index a61f3036b..42568c202 100644
--- a/app/javascript/src/styles/common/fonts.css
+++ b/app/javascript/src/styles/common/fonts.css
@@ -320,3 +320,12 @@
local("Anarchy"),
url("../../../../../public/fonts/Anarchy.ttf") format("truetype");
}
+
+/* https://www.1001freefonts.com/gisele-script.font */
+@font-face {
+ font-family: "Childlike";
+ font-display: swap;
+ src:
+ local("Gisele Script"),
+ url("../../../../../public/fonts/gisele.ttf") format("truetype");
+}
diff --git a/public/fonts/gisele.ttf b/public/fonts/gisele.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..d04d1c3309e89df1df9cd172f22e0be2b5763c8e
GIT binary patch
literal 13356
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
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 002/132] posts: remove "repopulated 1 old tag" message.
Remove the "Repopulated 1 old tag" message. Show "Created 1 new tag"
instead. The distinction between creating a brand new tag and
repopulating an empty tag doesn't matter.
---
app/models/post.rb | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/app/models/post.rb b/app/models/post.rb
index 8a33ac746..5117102f2 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1377,9 +1377,7 @@ class Post < ApplicationRecord
def added_tags_are_valid
new_tags = added_tags.select(&:empty?)
- new_general_tags = new_tags.select(&:general?)
- new_artist_tags = new_tags.select(&:artist?)
- repopulated_tags = new_tags.select { |t| !t.general? && !t.meta? && (t.created_at < 1.hour.ago) }
+ new_artist_tags, new_general_tags = new_tags.partition(&:artist?)
if new_general_tags.present?
n = new_general_tags.size
@@ -1387,12 +1385,6 @@ class Post < ApplicationRecord
self.warnings[:base] << "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
end
- if repopulated_tags.present?
- n = repopulated_tags.size
- tag_wiki_links = repopulated_tags.map { |tag| "[[#{tag.name}]]" }
- self.warnings[:base] << "Repopulated #{n} old #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
- end
-
new_artist_tags.each do |tag|
if tag.artist.blank?
self.warnings[:base] << "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]"
From adc1c2c2cc1b9e76e08257cff30e7a45076f6066 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 003/132] autocomplete: refactor javascript to use
/autocomplete endpoint.
This refactors the autocomplete Javascript to use a single dedicated
/autocomplete.json endpoint instead of a bunch of separate endpoints.
This simplifies the autocomplete Javascript by making it so that instead
of calling a different endpoint for each type of query (for users, wiki
pages, pools, artists, etc), then having to parse the results of each
call to get the data we need, we can call a single endpoint that returns
exactly what we need.
This also means we don't have to parse searches clientside in order to
autocomplete metatags. Instead we can just pass the search term to the
server and let it parse the search, which is easy to do serverside.
Finally, this makes autocomplete easier to test, and it makes it easier
to add more sophisticated autocomplete behavior, since most of the logic
lives serverside.
---
app/controllers/autocomplete_controller.rb | 15 +-
.../src/javascripts/autocomplete.js.erb | 260 ++----------------
app/logical/autocomplete_service.rb | 165 +++++++++++
app/models/artist.rb | 4 +
app/models/wiki_page.rb | 4 +
app/views/static/opensearch.xml.erb | 2 +-
.../autocomplete_controller_test.rb | 26 +-
test/unit/autocomplete_service_test.rb | 120 ++++++++
8 files changed, 345 insertions(+), 251 deletions(-)
create mode 100644 app/logical/autocomplete_service.rb
create mode 100644 test/unit/autocomplete_service_test.rb
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 37fa3565b..68c46d793 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -2,15 +2,12 @@ class AutocompleteController < ApplicationController
respond_to :xml, :json
def index
- @tags = Tag.names_matches_with_aliases(params[:query], params.fetch(:limit, 10).to_i)
+ @query = params.dig(:search, :query)
+ @type = params.dig(:search, :type)
+ @limit = params.fetch(:limit, 10).to_i
+ @autocomplete = AutocompleteService.new(@query, @type, current_user: CurrentUser.user, limit: @limit)
- if request.variant.opensearch?
- expires_in 1.hour
- results = [params[:query], @tags.map(&:pretty_name)]
- respond_with(results)
- else
- # XXX
- respond_with(@tags.map(&:attributes))
- end
+ @results = @autocomplete.autocomplete_results
+ respond_with(@results)
end
end
diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index a1d32f478..1c81f3651 100644
--- a/app/javascript/src/javascripts/autocomplete.js.erb
+++ b/app/javascript/src/javascripts/autocomplete.js.erb
@@ -3,16 +3,10 @@ import CurrentUser from './current_user'
let Autocomplete = {};
/* eslint-disable */
-Autocomplete.METATAGS = <%= PostQueryBuilder::METATAGS.to_json.html_safe %>;
Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.to_json.html_safe %>;
-Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_safe %>;
-Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
/* eslint-enable */
-Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated", "appealed"];
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
-Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
-Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
Autocomplete.MAX_RESULTS = 10;
Autocomplete.initialize_all = function() {
@@ -39,20 +33,20 @@ Autocomplete.initialize_all = function() {
this.initialize_tag_autocomplete();
this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
- this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source);
- this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source);
- this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source);
- this.initialize_fields($('[data-autocomplete="user"]'), Autocomplete.user_source);
- this.initialize_fields($('[data-autocomplete="wiki-page"]'), Autocomplete.wiki_source);
- this.initialize_fields($('[data-autocomplete="favorite-group"]'), Autocomplete.favorite_group_source);
- this.initialize_fields($('[data-autocomplete="saved-search-label"]'), Autocomplete.saved_search_source);
+ this.initialize_fields($('[data-autocomplete="tag"]'), "tag");
+ this.initialize_fields($('[data-autocomplete="artist"]'), "artist");
+ this.initialize_fields($('[data-autocomplete="pool"]'), "pool");
+ this.initialize_fields($('[data-autocomplete="user"]'), "user");
+ this.initialize_fields($('[data-autocomplete="wiki-page"]'), "wiki_page");
+ this.initialize_fields($('[data-autocomplete="favorite-group"]'), "favorite_group");
+ this.initialize_fields($('[data-autocomplete="saved-search-label"]'), "saved_search_label");
}
}
-Autocomplete.initialize_fields = function($fields, autocomplete) {
+Autocomplete.initialize_fields = function($fields, type) {
$fields.autocomplete({
source: async function(request, respond) {
- let results = await autocomplete(request.term);
+ let results = await Autocomplete.autocomplete_source(request.term, type);
respond(results);
},
});
@@ -84,7 +78,7 @@ Autocomplete.initialize_mention_autocomplete = function($fields) {
}
if (name) {
- let results = await Autocomplete.user_source(name, "@");
+ let results = await Autocomplete.autocomplete_source(name, "mention");
resp(results);
}
}
@@ -106,76 +100,18 @@ Autocomplete.initialize_tag_autocomplete = function() {
return false;
},
source: async function(req, resp) {
- var query = Autocomplete.parse_query(req.term, this.element.get(0).selectionStart);
- var metatag = query.metatag;
- var term = query.term;
- var results = [];
-
- switch (metatag) {
- case "order":
- case "status":
- case "rating":
- case "locked":
- case "child":
- case "parent":
- case "filetype":
- case "disapproved":
- case "embedded":
- results = Autocomplete.static_metatag_source(term, metatag);
- break;
- case "user":
- case "approver":
- case "commenter":
- case "comm":
- case "noter":
- case "noteupdater":
- case "commentaryupdater":
- case "artcomm":
- case "fav":
- case "ordfav":
- case "appealer":
- case "flagger":
- case "upvote":
- case "downvote":
- results = await Autocomplete.user_source(term, metatag + ":");
- break;
- case "pool":
- case "ordpool":
- results = await Autocomplete.pool_source(term, metatag + ":");
- break;
- case "favgroup":
- case "ordfavgroup":
- results = await Autocomplete.favorite_group_source(term, metatag + ":", CurrentUser.data("id"));
- break;
- case "search":
- results = await Autocomplete.saved_search_source(term, metatag + ":");
- break;
- case "tag":
- results = await Autocomplete.tag_source(term);
- break;
- default:
- results = [];
- break;
- }
-
+ let term = Autocomplete.current_term(this.element);
+ let results = await Autocomplete.autocomplete_source(term, "tag_query");
resp(results);
}
});
}
-Autocomplete.parse_query = function(text, caret) {
- let before_caret_text = text.substring(0, caret);
- let match = before_caret_text.match(Autocomplete.TERM_REGEX);
-
- let operator = match[1];
- let metatag = match[2] ? match[2].toLowerCase() : "tag";
- let term = match[3];
-
- if (metatag in Autocomplete.TAG_CATEGORIES) {
- metatag = "tag";
- }
-
- return { operator: operator, metatag: metatag, term: term };
+Autocomplete.current_term = function($input) {
+ let query = $input.get(0).value;
+ let caret = $input.get(0).selectionStart;
+ let match = query.substring(0, caret).match(/\S*/);
+ return match[0];
};
// Update the input field with the item currently focused in the
@@ -264,166 +200,12 @@ Autocomplete.render_item = function(list, item) {
return $list_item.appendTo(list);
};
-Autocomplete.static_metatags = {
- order: Autocomplete.ORDER_METATAGS,
- status: ["any"].concat(Autocomplete.MISC_STATUSES),
- rating: [
- "safe", "questionable", "explicit"
- ],
- locked: [
- "rating", "note", "status"
- ],
- embedded: [
- "true", "false"
- ],
- child: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
- parent: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
- filetype: [
- "jpg", "png", "gif", "swf", "zip", "webm", "mp4"
- ],
- commentary: [
- "true", "false", "translated", "untranslated"
- ],
- disapproved: Autocomplete.DISAPPROVAL_REASONS
-}
-
-Autocomplete.static_metatag_source = function(term, metatag) {
- var sub_metatags = this.static_metatags[metatag];
-
- var matches = sub_metatags.filter(sub_metatag => sub_metatag.startsWith(term.toLowerCase()));
- matches = matches.map(sub_metatag => `${metatag}:${sub_metatag}`).sort().slice(0, Autocomplete.MAX_RESULTS);
-
- return matches;
-}
-
-Autocomplete.tag_source = async function(term) {
- if (term === "") {
- return [];
- }
-
- let tags = await $.getJSON("/tags/autocomplete", {
- "search[name_matches]": term,
- "limit": Autocomplete.MAX_RESULTS,
- "expiry": 7
- });
-
- return tags.map(function(tag) {
- return {
- type: "tag",
- label: tag.name.replace(/_/g, " "),
- antecedent: tag.antecedent_name,
- value: tag.name,
- category: tag.category,
- source: tag.source,
- weight: tag.weight,
- post_count: tag.post_count
- };
- });
-}
-
-Autocomplete.artist_source = async function(term) {
- let artists = await $.getJSON("/artists", {
- "search[name_like]": term.trim().replace(/\s+/g, "_") + "*",
- "search[is_deleted]": false,
- "search[order]": "post_count",
- "limit": Autocomplete.MAX_RESULTS,
- "expiry": 7
- });
-
- return artists.map(function(artist) {
- return {
- type: "tag",
- label: artist.name.replace(/_/g, " "),
- value: artist.name,
- category: Autocomplete.TAG_CATEGORIES.artist,
- };
- });
-};
-
-Autocomplete.wiki_source = async function(term) {
- let wiki_pages = await $.getJSON("/wiki_pages", {
- "search[title_normalize]": term + "*",
- "search[hide_deleted]": "Yes",
- "search[order]": "post_count",
- "limit": Autocomplete.MAX_RESULTS,
- "expiry": 7
- });
-
- return wiki_pages.map(function(wiki_page) {
- return {
- type: "tag",
- label: wiki_page.title.replace(/_/g, " "),
- value: wiki_page.title,
- category: wiki_page.category_name
- };
- });
-};
-
-Autocomplete.user_source = async function(term, prefix = "") {
- let users = await $.getJSON("/users", {
- "search[order]": "post_upload_count",
- "search[current_user_first]": "true",
- "search[name_matches]": term + "*",
+Autocomplete.autocomplete_source = function(query, type) {
+ return $.getJSON("/autocomplete", {
+ "search[query]": query,
+ "search[type]": type,
"limit": Autocomplete.MAX_RESULTS
});
-
- return users.map(function(user) {
- return {
- type: "user",
- label: user.name.replace(/_/g, " "),
- value: prefix + user.name,
- level: user.level_string
- };
- });
-};
-
-Autocomplete.pool_source = async function(term, prefix = "") {
- let pools = await $.getJSON("/pools", {
- "search[name_matches]": term,
- "search[is_deleted]": false,
- "search[order]": "post_count",
- "limit": Autocomplete.MAX_RESULTS
- });
-
- return pools.map(function(pool) {
- return {
- type: "pool",
- label: pool.name.replace(/_/g, " "),
- value: prefix + pool.name,
- post_count: pool.post_count,
- category: pool.category
- };
- });
-};
-
-Autocomplete.favorite_group_source = async function(term, prefix = "", creator_id = null) {
- let favgroups = await $.getJSON("/favorite_groups", {
- "search[creator_id]": creator_id,
- "search[name_matches]": term,
- "limit": Autocomplete.MAX_RESULTS
- });
-
- return favgroups.map(function(favgroup) {
- return {
- label: favgroup.name.replace(/_/g, " "),
- value: prefix + favgroup.name,
- post_count: favgroup.post_count
- };
- });
-};
-
-Autocomplete.saved_search_source = async function(term, prefix = "") {
- let labels = await $.getJSON("/saved_searches/labels", {
- "search[label]": term + "*",
- "limit": Autocomplete.MAX_RESULTS
- });
-
- return labels.map(function(label) {
- return {
- label: label.replace(/_/g, " "),
- value: prefix + label,
- };
- });
}
$(document).ready(function() {
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
new file mode 100644
index 000000000..4896bf225
--- /dev/null
+++ b/app/logical/autocomplete_service.rb
@@ -0,0 +1,165 @@
+class AutocompleteService
+ POST_STATUSES = %w[active deleted pending flagged appealed banned modqueue unmoderated]
+
+ STATIC_METATAGS = {
+ status: %w[any] + POST_STATUSES,
+ child: %w[any none] + POST_STATUSES,
+ parent: %w[any none] + POST_STATUSES,
+ rating: %w[safe questionable explicit],
+ locked: %w[rating note status],
+ embedded: %w[true false],
+ filetype: %w[jpg png gif swf zip webm mp4],
+ commentary: %w[true false translated untranslated],
+ disapproved: PostDisapproval::REASONS,
+ order: PostQueryBuilder::ORDER_METATAGS
+ }
+
+ attr_reader :query, :type, :limit, :current_user
+
+ def initialize(query, type, current_user: User.anonymous, limit: 10)
+ @query = query.to_s
+ @type = type.to_sym
+ @current_user = current_user
+ @limit = limit
+ end
+
+ def autocomplete_results
+ case type
+ when :tag_query
+ autocomplete_tag_query(query)
+ when :tag
+ autocomplete_tag(query)
+ when :artist
+ autocomplete_artist(query)
+ when :wiki_page
+ autocomplete_wiki_page(query)
+ when :user
+ autocomplete_user(query)
+ when :mention
+ autocomplete_mention(query)
+ when :pool
+ autocomplete_pool(query)
+ when :favorite_group
+ autocomplete_favorite_group(query)
+ when :saved_search_label
+ autocomplete_saved_search_label(query)
+ when :opensearch
+ autocomplete_opensearch(query)
+ else
+ []
+ end
+ end
+
+ def autocomplete_tag_query(string)
+ term = PostQueryBuilder.new(string).terms.first
+ return [] if term.nil?
+
+ case term.type
+ when :tag
+ autocomplete_tag(term.name)
+ when :metatag
+ autocomplete_metatag(term.name, term.value)
+ end
+ end
+
+ def autocomplete_tag(string)
+ tags = Tag.names_matches_with_aliases(string, limit)
+
+ tags.map do |tag|
+ { type: "tag", label: tag.name.tr("_", " "), value: tag.name, antecedent: tag.antecedent_name, category: tag.category, post_count: tag.post_count, source: nil, weight: nil }
+ end
+ end
+
+ def autocomplete_metatag(metatag, value)
+ results = case metatag.to_sym
+ when :user, :approver, :commenter, :comm, :noter, :noteupdater, :commentaryupdater,
+ :artcomm, :fav, :ordfav, :appealer, :flagger, :upvote, :downvote
+ autocomplete_user(value)
+ when :pool, :ordpool
+ autocomplete_pool(value)
+ when :favgroup, :ordfavgroup
+ autocomplete_favorite_group(value)
+ when :search
+ autocomplete_saved_search_label(value)
+ when *STATIC_METATAGS.keys
+ autocomplete_static_metatag(metatag, value)
+ end
+
+ results.map do |result|
+ { **result, value: metatag + ":" + result[:value] }
+ end
+ end
+
+ def autocomplete_static_metatag(metatag, value)
+ values = STATIC_METATAGS[metatag.to_sym]
+ results = values.select { |v| v.starts_with?(value) }.sort.take(limit)
+
+ results.map do |v|
+ { label: metatag + ":" + v, value: v }
+ end
+ end
+
+ def autocomplete_pool(string)
+ string = "*" + string + "*" unless string.include?("*")
+ pools = Pool.undeleted.name_matches(string).search(order: "post_count").limit(limit)
+
+ pools.map do |pool|
+ { type: "pool", label: pool.pretty_name, value: pool.name, post_count: pool.post_count, category: pool.category }
+ end
+ end
+
+ def autocomplete_favorite_group(string)
+ string = "*" + string + "*" unless string.include?("*")
+ favgroups = FavoriteGroup.visible(current_user).where(creator: current_user).name_matches(string).search(order: "post_count").limit(limit)
+
+ favgroups.map do |favgroup|
+ { label: favgroup.pretty_name, value: favgroup.name, post_count: favgroup.post_count }
+ end
+ end
+
+ def autocomplete_saved_search_label(string)
+ labels = SavedSearch.search_labels(current_user.id, label: string).take(limit)
+
+ labels.map do |label|
+ { label: label.tr("_", " "), value: label }
+ end
+ end
+
+ def autocomplete_artist(string)
+ string = string + "*" unless string.include?("*")
+ artists = Artist.undeleted.name_matches(string).search(order: "post_count").limit(limit)
+
+ artists.map do |artist|
+ { type: "tag", label: artist.pretty_name, value: artist.name, category: Tag.categories.artist }
+ end
+ end
+
+ def autocomplete_wiki_page(string)
+ string = string + "*" unless string.include?("*")
+ wiki_pages = WikiPage.undeleted.title_matches(string).search(order: "post_count").limit(limit)
+
+ wiki_pages.map do |wiki_page|
+ { type: "tag", label: wiki_page.pretty_title, value: wiki_page.title, category: wiki_page.tag&.category }
+ end
+ end
+
+ def autocomplete_user(string)
+ string = string + "*" unless string.include?("*")
+ users = User.search(name_matches: string, current_user_first: true, order: "post_upload_count").limit(limit)
+
+ users.map do |user|
+ { type: "user", label: user.pretty_name, value: user.name, level: user.level_string }
+ end
+ end
+
+ def autocomplete_mention(string)
+ autocomplete_user(string).map do |result|
+ { **result, value: "@" + result[:value] }
+ end
+ end
+
+ def autocomplete_opensearch(string)
+ results = autocomplete_tag(string).map { |result| result[:value] }
+ [query, results]
+ end
+end
diff --git a/app/models/artist.rb b/app/models/artist.rb
index cc604789c..abc7260bf 100644
--- a/app/models/artist.rb
+++ b/app/models/artist.rb
@@ -203,6 +203,10 @@ class Artist < ApplicationRecord
end
module SearchMethods
+ def name_matches(query)
+ where_like(:name, normalize_name(query))
+ end
+
def any_other_name_matches(regex)
where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex))
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e10c1312f..6f914bd25 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -33,6 +33,10 @@ class WikiPage < ApplicationRecord
where(title: normalize_title(title))
end
+ def title_matches(title)
+ where_like(:title, normalize_title(title))
+ end
+
def other_names_include(name)
name = normalize_other_name(name)
subquery = WikiPage.from("unnest(other_names) AS other_name").where_iequals("other_name", name)
diff --git a/app/views/static/opensearch.xml.erb b/app/views/static/opensearch.xml.erb
index 8652a2f9c..ce8737643 100644
--- a/app/views/static/opensearch.xml.erb
+++ b/app/views/static/opensearch.xml.erb
@@ -4,5 +4,5 @@
<%= Danbooru.config.app_name %> search
<%= root_url %>favicon.ico
-
+
diff --git a/test/functional/autocomplete_controller_test.rb b/test/functional/autocomplete_controller_test.rb
index 51c064029..4dd62ee1a 100644
--- a/test/functional/autocomplete_controller_test.rb
+++ b/test/functional/autocomplete_controller_test.rb
@@ -1,6 +1,17 @@
require "test_helper"
class AutocompleteControllerTest < ActionDispatch::IntegrationTest
+ def autocomplete(query, type)
+ get autocomplete_index_path(search: { query: query, type: type }), as: :json
+ assert_response :success
+
+ response.parsed_body.map { |result| result["value"] }
+ end
+
+ def assert_autocomplete_equals(expected_value, query, type)
+ assert_equal(expected_value, autocomplete(query, type))
+ end
+
context "Autocomplete controller" do
context "index action" do
setup do
@@ -8,9 +19,20 @@ class AutocompleteControllerTest < ActionDispatch::IntegrationTest
end
should "work for opensearch queries" do
- get autocomplete_index_path(query: "azur", variant: "opensearch"), as: :json
+ get autocomplete_index_path(search: { query: "azur", type: "opensearch" }), as: :json
+
assert_response :success
- assert_equal(["azur", ["azur lane"]], response.parsed_body)
+ assert_equal(["azur", ["azur_lane"]], response.parsed_body)
+ end
+
+ should "work for tag queries" do
+ assert_autocomplete_equals(["azur_lane"], "azur", "tag_query")
+ assert_autocomplete_equals(["azur_lane"], "-azur", "tag_query")
+ assert_autocomplete_equals(["azur_lane"], "~azur", "tag_query")
+ assert_autocomplete_equals(["azur_lane"], "AZUR", "tag_query")
+
+ assert_autocomplete_equals(["rating:safe"], "rating:s", "tag_query")
+ assert_autocomplete_equals(["rating:safe"], "-rating:s", "tag_query")
end
end
end
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
new file mode 100644
index 000000000..fdcf7aeb7
--- /dev/null
+++ b/test/unit/autocomplete_service_test.rb
@@ -0,0 +1,120 @@
+require 'test_helper'
+
+class AutocompleteServiceTest < ActiveSupport::TestCase
+ def autocomplete(query, type, **options)
+ results = AutocompleteService.new(query, type, **options).autocomplete_results
+ results.map { |r| r[:value] }
+ end
+
+ def assert_autocomplete_includes(expected_value, query, type, **options)
+ assert_includes(autocomplete(query, type, **options), expected_value)
+ end
+
+ def assert_autocomplete_equals(expected_value, query, type, **options)
+ assert_equal(expected_value, autocomplete(query, type, **options))
+ end
+
+ context "#autocomplete method" do
+ should "autocomplete artists" do
+ create(:artist, name: "bkub")
+ assert_autocomplete_includes("bkub", "bk", :artist)
+ end
+
+ should "autocomplete wiki pages" do
+ create(:wiki_page, title: "help:home")
+ assert_autocomplete_includes("help:home", "help", :wiki_page)
+ end
+
+ should "autocomplete users" do
+ @user = create(:user, name: "fumimi")
+
+ as(@user) do
+ assert_autocomplete_includes("fumimi", "fu", :user)
+ assert_autocomplete_includes("@fumimi", "fu", :mention)
+ assert_autocomplete_includes("user:fumimi", "user:fu", :tag_query)
+ end
+ end
+
+ should "autocomplete pools" do
+ as(create(:user)) do
+ create(:pool, name: "Disgustingly_Adorable")
+ end
+
+ assert_autocomplete_includes("Disgustingly_Adorable", "disgust", :pool)
+ assert_autocomplete_includes("pool:Disgustingly_Adorable", "pool:disgust", :tag_query)
+ assert_autocomplete_includes("pool:Disgustingly_Adorable", "-pool:disgust", :tag_query)
+ end
+
+ should "autocomplete favorite groups" do
+ user = create(:user)
+ create(:favorite_group, name: "Stuff", creator: user)
+
+ assert_autocomplete_equals(["Stuff"], "stu", :favorite_group, current_user: user)
+ assert_autocomplete_equals([], "stu", :favorite_group, current_user: User.anonymous)
+
+ assert_autocomplete_equals(["favgroup:Stuff"], "favgroup:stu", :tag_query, current_user: user)
+ assert_autocomplete_equals([], "favgroup:stu", :tag_query, current_user: User.anonymous)
+ end
+
+ should "autocomplete saved search labels" do
+ user = create(:user)
+ create(:saved_search, query: "bkub", labels: ["artists"], user: user)
+
+ assert_autocomplete_equals(["artists"], "art", :saved_search_label, current_user: user)
+
+ assert_autocomplete_equals(["search:artists"], "search:art", :tag_query, current_user: user)
+ end
+
+ should "autocomplete single tags" do
+ create(:tag, name: "touhou")
+ assert_autocomplete_includes("touhou", "tou", :tag)
+ end
+
+ context "for a tag search" do
+ should "autocomplete tags" do
+ create(:tag, name: "touhou")
+
+ assert_autocomplete_includes("touhou", "tou", :tag_query)
+ assert_autocomplete_includes("touhou", "TOU", :tag_query)
+ assert_autocomplete_includes("touhou", "-tou", :tag_query)
+ assert_autocomplete_includes("touhou", "~tou", :tag_query)
+ end
+
+ should "autocomplete static metatags" do
+ assert_autocomplete_equals(["status:active"], "status:act", :tag_query)
+ assert_autocomplete_equals(["parent:active"], "parent:act", :tag_query)
+ assert_autocomplete_equals(["child:active"], "child:act", :tag_query)
+
+ assert_autocomplete_equals(["rating:safe"], "rating:s", :tag_query)
+ assert_autocomplete_equals(["rating:questionable"], "rating:q", :tag_query)
+ assert_autocomplete_equals(["rating:explicit"], "rating:e", :tag_query)
+
+ assert_autocomplete_equals(["locked:rating"], "locked:r", :tag_query)
+ assert_autocomplete_equals(["locked:status"], "locked:s", :tag_query)
+ assert_autocomplete_equals(["locked:note"], "locked:n", :tag_query)
+
+ assert_autocomplete_equals(["embedded:true"], "embedded:t", :tag_query)
+ assert_autocomplete_equals(["embedded:false"], "embedded:f", :tag_query)
+
+ assert_autocomplete_equals(["filetype:jpg"], "filetype:j", :tag_query)
+ assert_autocomplete_equals(["filetype:png"], "filetype:p", :tag_query)
+ assert_autocomplete_equals(["filetype:gif"], "filetype:g", :tag_query)
+ assert_autocomplete_equals(["filetype:swf"], "filetype:s", :tag_query)
+ assert_autocomplete_equals(["filetype:zip"], "filetype:z", :tag_query)
+ assert_autocomplete_equals(["filetype:webm"], "filetype:w", :tag_query)
+ assert_autocomplete_equals(["filetype:mp4"], "filetype:m", :tag_query)
+
+ assert_autocomplete_equals(["commentary:true"], "commentary:tru", :tag_query)
+ assert_autocomplete_equals(["commentary:false"], "commentary:fal", :tag_query)
+ assert_autocomplete_equals(["commentary:translated"], "commentary:trans", :tag_query)
+ assert_autocomplete_equals(["commentary:untranslated"], "commentary:untrans", :tag_query)
+
+ assert_autocomplete_equals(["disapproved:breaks_rules"], "disapproved:break", :tag_query)
+ assert_autocomplete_equals(["disapproved:poor_quality"], "disapproved:poor", :tag_query)
+ assert_autocomplete_equals(["disapproved:disinterest"], "disapproved:dis", :tag_query)
+
+ assert_autocomplete_equals(["order:score", "order:score_asc"], "order:sco", :tag_query)
+ end
+ end
+ end
+end
From b0be8ae4562644469cd481ae3e8a075e040c4bc4 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 004/132] autocomplete: rework tag autocomplete behavior.
Reworks tag autocomplete to work the same way for all users. Previously
autocomplete for Builders worked differently than autocomplete for
regular users.
This is how it works now:
* If the search starts with a slash (/), then do a tag abbreviation
match. For example, `/evth` matches eyebrows_visible_through_hair.
* Otherwise if the search contains a wildcard (*), then just do a simple
wildcard search.
* Otherwise do a tag prefix match against tags and aliases. For example,
`black` matches all tags or aliases beginning with `black`.
* If the tag prefix match returns no results, then do a autocorrect match.
The differences for regular users:
* You can abbreviate tags with a slash (/).
The differences for Builders:
* Now tag abbreviations have to start with a slash (/).
* Autocorrect isn't performed unless a regular search returns no results.
* Results are always sorted by tag count. Before different types of
results (regular tag matches, alias matches, abbreviation matches,
and autocorrect matches) were all mixed together based on a tag
weighting scheme.
---
app/helpers/tags_helper.rb | 8 ------
app/logical/autocomplete_service.rb | 39 ++++++++++++++++++++++++--
app/models/tag.rb | 21 +++++++++++++-
app/views/tags/index.html.erb | 2 +-
test/unit/autocomplete_service_test.rb | 31 ++++++++++++++++++++
5 files changed, 89 insertions(+), 12 deletions(-)
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index 53f0e01e7..d6e8a9728 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -3,12 +3,4 @@ module TagsHelper
return nil if tag.blank?
"tag-type-#{tag.category}"
end
-
- def tag_alias_for_pattern(tag, pattern)
- return nil if pattern.blank?
-
- tag.consequent_aliases.find do |tag_alias|
- !tag.name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern)
- end
- end
end
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 4896bf225..d05cb6696 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -63,10 +63,45 @@ class AutocompleteService
end
def autocomplete_tag(string)
- tags = Tag.names_matches_with_aliases(string, limit)
+ if string.starts_with?("/")
+ string = string + "*" unless string.include?("*")
+ results = tag_matches(string)
+ results += tag_abbreviation_matches(string)
+ results = results.uniq.sort_by { |r| [r[:antecedent].length, -r[:post_count]] }.take(limit)
+ elsif string.include?("*")
+ results = tag_matches(string)
+ else
+ results = tag_matches(string + "*")
+ results = tag_autocorrect_matches(string) if results.blank?
+ end
+
+ results
+ end
+
+ def tag_matches(string)
+ name_matches = Tag.nonempty.name_matches(string).order(post_count: :desc).limit(limit)
+ alias_matches = Tag.nonempty.alias_matches(string).order(post_count: :desc).limit(limit)
+ union = "((#{name_matches.to_sql}) UNION (#{alias_matches.to_sql})) AS tags"
+ tags = Tag.from(union).order(post_count: :desc).limit(limit).includes(:consequent_aliases)
tags.map do |tag|
- { type: "tag", label: tag.name.tr("_", " "), value: tag.name, antecedent: tag.antecedent_name, category: tag.category, post_count: tag.post_count, source: nil, weight: nil }
+ { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: tag.tag_alias_for_pattern(string)&.antecedent_name }
+ end
+ end
+
+ def tag_abbreviation_matches(string)
+ tags = Tag.nonempty.abbreviation_matches(string).order(post_count: :desc).limit(limit)
+
+ tags.map do |tag|
+ { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: "/" + tag.abbreviation }
+ end
+ end
+
+ def tag_autocorrect_matches(string)
+ tags = Tag.nonempty.fuzzy_name_matches(string).order_similarity(string).limit(limit)
+
+ tags.map do |tag|
+ { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count }
end
end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index c9ca52da9..899718add 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1,4 +1,6 @@
class Tag < ApplicationRecord
+ ABBREVIATION_REGEXP = /([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)/
+
has_one :wiki_page, :foreign_key => "title", :primary_key => "name"
has_one :artist, :foreign_key => "name", :primary_key => "name"
has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name"
@@ -249,7 +251,7 @@ class Tag < ApplicationRecord
end
def alias_matches(name)
- where(name: TagAlias.active.where_ilike(:antecedent_name, normalize_name(name)).select(:consequent_name))
+ where(name: TagAlias.active.where_like(:antecedent_name, normalize_name(name)).select(:consequent_name))
end
def name_or_alias_matches(name)
@@ -260,6 +262,11 @@ class Tag < ApplicationRecord
nonempty.name_matches(tag).order(post_count: :desc, name: :asc).limit(limit).pluck(:name)
end
+ def abbreviation_matches(abbrev)
+ abbrev = abbrev.delete_prefix("/")
+ where("regexp_replace(tags.name, ?, '\\1', 'g') LIKE ?", ABBREVIATION_REGEXP.source, abbrev.to_escaped_for_sql_like)
+ end
+
def search(params)
q = super
@@ -352,6 +359,18 @@ class Tag < ApplicationRecord
Post.system_tag_match(name)
end
+ def abbreviation
+ name.gsub(ABBREVIATION_REGEXP, "\\1")
+ end
+
+ def tag_alias_for_pattern(pattern)
+ return nil if pattern.blank?
+
+ consequent_aliases.find do |tag_alias|
+ !name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern)
+ end
+ end
+
def self.model_restriction(table)
super.where(table[:post_count].gt(0))
end
diff --git a/app/views/tags/index.html.erb b/app/views/tags/index.html.erb
index 8648b1466..d884a56cb 100644
--- a/app/views/tags/index.html.erb
+++ b/app/views/tags/index.html.erb
@@ -10,7 +10,7 @@
<%= link_to_wiki "?", tag.name, class: tag_class(tag) %>
<%= link_to tag.name, posts_path(tags: tag.name), class: tag_class(tag) %>
- <% tag_alias = tag_alias_for_pattern(tag, params[:search][:name_or_alias_matches]) %>
+ <% tag_alias = tag.tag_alias_for_pattern(params[:search][:name_or_alias_matches]) %>
<% if tag_alias.present? %>
← <%= link_to tag_alias.antecedent_name, tag_alias, class: "fineprint" %>
<% end %>
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index fdcf7aeb7..8379bb295 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -80,6 +80,37 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_includes("touhou", "~tou", :tag_query)
end
+ should "autocomplete tag abbreviations" do
+ create(:tag, name: "mole", post_count: 150)
+ create(:tag, name: "mole_under_eye", post_count: 100)
+ create(:tag, name: "mole_under_mouth", post_count: 50)
+
+ assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "/m", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "/mu", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_mouth], "/mum", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye], "/mue", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye], "/*ue", :tag_query)
+
+ assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
+ assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
+ end
+
+ should "autocomplete wildcard searches" do
+ create(:tag, name: "mole", post_count: 150)
+ create(:tag, name: "mole_under_eye", post_count: 100)
+ create(:tag, name: "mole_under_mouth", post_count: 50)
+
+ assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "mole*", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "*under*", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye], "*eye", :tag_query)
+ end
+
+ should "autocorrect misspelled tags" do
+ create(:tag, name: "touhou")
+
+ assert_autocomplete_equals(%w[touhou], "touhuo", :tag_query)
+ end
+
should "autocomplete static metatags" do
assert_autocomplete_equals(["status:active"], "status:act", :tag_query)
assert_autocomplete_equals(["parent:active"], "parent:act", :tag_query)
From d6a5b9e2527613bd8f4c4677aac2ec9e65e0f3c8 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 005/132] autocomplete: rework cache policy.
The previous cache policy was that all autocomplete results were cached
for a fixed 7 days. The new policy is that if autocomplete returns more
than 10 results they're cached for 24 hours, otherwise if it returns
less than 10 results they're cached for 1 hour.
The rationale is that if autocomplete returns a lot of results, then the
top 10 results are relatively stable and unlikely to change, but if it
returns less than 10 results, then the results are unstable and can be
easily changed.
We also change it so that autocomplete calls can be cached publicly.
Public caching means that HTTP requests are cached by Cloudflare. This
will ideally reduce load on the server and reduce latency for end users.
This is only safe for calls that return the same results for all users
(i.e. the results don't depend on the current user), since the cache is
publicly shared by all users. Currently username, favgroup, and saved
search autocomplete results depend on the current user, so they can't be
publicly cached.
---
app/controllers/autocomplete_controller.rb | 4 ++++
app/logical/autocomplete_service.rb | 27 ++++++++++++++++++++++
2 files changed, 31 insertions(+)
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 68c46d793..0b36168db 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -8,6 +8,10 @@ class AutocompleteController < ApplicationController
@autocomplete = AutocompleteService.new(@query, @type, current_user: CurrentUser.user, limit: @limit)
@results = @autocomplete.autocomplete_results
+ @expires_in = @autocomplete.cache_duration
+ @public = @autocomplete.cache_publicly?
+
+ expires_in @expires_in, public: @public unless response.cache_control.present?
respond_with(@results)
end
end
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index d05cb6696..8ae91392d 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -1,4 +1,6 @@
class AutocompleteService
+ extend Memoist
+
POST_STATUSES = %w[active deleted pending flagged appealed banned modqueue unmoderated]
STATIC_METATAGS = {
@@ -197,4 +199,29 @@ class AutocompleteService
results = autocomplete_tag(string).map { |result| result[:value] }
[query, results]
end
+
+ def cache_duration
+ if autocomplete_results.size == limit
+ 24.hours
+ else
+ 1.hour
+ end
+ end
+
+ # Queries that don't depend on the current user are safe to cache publicly.
+ def cache_publicly?
+ if type == :tag_query && parsed_search&.type == :tag
+ true
+ elsif type.in?(%i[tag artist wiki_page pool opensearch])
+ true
+ else
+ false
+ end
+ end
+
+ def parsed_search
+ PostQueryBuilder.new(query).terms.first
+ end
+
+ memoize :autocomplete_results
end
From 119268e118f64bb45b8e2700b4b44c2cf726f6a4 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 006/132] autocomplete: fix exception when completing saved
search labels.
Fix an exception that was thrown when trying to autocomplete saved
search labels (e.g. `search:all`) as an anonymous user. This was a
pre-existing bug.
---
app/logical/autocomplete_service.rb | 3 ++-
app/models/saved_search.rb | 8 ++++++++
test/unit/autocomplete_service_test.rb | 2 ++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 8ae91392d..8ec4d6a23 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -155,7 +155,8 @@ class AutocompleteService
end
def autocomplete_saved_search_label(string)
- labels = SavedSearch.search_labels(current_user.id, label: string).take(limit)
+ string = "*" + string + "*" unless string.include?("*")
+ labels = current_user.saved_searches.labels_like(string).take(limit)
labels.map do |label|
{ label: label.tr("_", " "), value: label }
diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb
index e384da9a1..9036608a9 100644
--- a/app/models/saved_search.rb
+++ b/app/models/saved_search.rb
@@ -63,6 +63,14 @@ class SavedSearch < ApplicationRecord
.gsub(/[[:space:]]/, "_")
end
+ def all_labels
+ select(Arel.sql("distinct unnest(labels) as label")).order(:label)
+ end
+
+ def labels_like(label)
+ all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
+ end
+
def search_labels(user_id, params)
labels = labels_for(user_id)
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index 8379bb295..29d8ea6ce 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -61,8 +61,10 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
create(:saved_search, query: "bkub", labels: ["artists"], user: user)
assert_autocomplete_equals(["artists"], "art", :saved_search_label, current_user: user)
+ assert_autocomplete_equals([], "art", :saved_search_label, current_user: User.anonymous)
assert_autocomplete_equals(["search:artists"], "search:art", :tag_query, current_user: user)
+ assert_autocomplete_equals([], "search:art", :saved_search_label, current_user: User.anonymous)
end
should "autocomplete single tags" do
From 6a46aeb55cebeccc5cfb03ed972435812d56cd4d Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 007/132] autocomplete: tune autocorrect algorithm.
Tune autocorrect to produce fewer false positives. Before we used
trigram similarity. Now we use Levenshtein edit distance with a dynamic
typo threshold. Trigram similarity was able to correct large
transpositions (e.g. `miku_hatsune` -> `hatsune_miku`), but it was bad
at correcting small typos. Levenshtein is good at small typos, but can't
correct large transpositions.
---
app/logical/autocomplete_service.rb | 2 +-
app/models/tag.rb | 11 +++++++----
...1213052805_add_extension_fuzzy_str_match.rb | 5 +++++
db/structure.sql | 18 +++++++++++++++++-
test/functional/tags_controller_test.rb | 2 +-
5 files changed, 31 insertions(+), 7 deletions(-)
create mode 100644 db/migrate/20201213052805_add_extension_fuzzy_str_match.rb
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 8ec4d6a23..33f5b6757 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -100,7 +100,7 @@ class AutocompleteService
end
def tag_autocorrect_matches(string)
- tags = Tag.nonempty.fuzzy_name_matches(string).order_similarity(string).limit(limit)
+ tags = Tag.nonempty.autocorrect_matches(string).limit(limit)
tags.map do |tag|
{ type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count }
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 899718add..3c3a3bf65 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -234,16 +234,19 @@ class Tag < ApplicationRecord
end
module SearchMethods
+ def autocorrect_matches(name)
+ tags = fuzzy_name_matches(name).order_similarity(name)
+ end
+
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
def order_similarity(name)
- # trunc(3 * sim) reduces the similarity score from a range of 0.0 -> 1.0 to just 0, 1, or 2.
- # This groups tags first by approximate similarity, then by largest tags within groups of similar tags.
- order(Arel.sql("trunc(3 * similarity(name, #{connection.quote(name)})) DESC"), "post_count DESC", "name DESC")
+ order(Arel.sql("levenshtein(left(name, 255), #{connection.quote(name)}), tags.post_count DESC, tags.name ASC"))
end
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
def fuzzy_name_matches(name)
- where("tags.name % ?", name)
+ max_distance = [name.size / 4, 3].max.floor.to_i
+ where("tags.name % ?", name).where("levenshtein(left(name, 255), ?) < ?", name, max_distance)
end
def name_matches(name)
diff --git a/db/migrate/20201213052805_add_extension_fuzzy_str_match.rb b/db/migrate/20201213052805_add_extension_fuzzy_str_match.rb
new file mode 100644
index 000000000..bea09b809
--- /dev/null
+++ b/db/migrate/20201213052805_add_extension_fuzzy_str_match.rb
@@ -0,0 +1,5 @@
+class AddExtensionFuzzyStrMatch < ActiveRecord::Migration[6.0]
+ def change
+ enable_extension "fuzzystrmatch"
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 7da011e05..d85f9a64c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9,6 +9,21 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
+
+--
+-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: -
+--
+
+CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public;
+
+
+--
+-- Name: EXTENSION fuzzystrmatch; Type: COMMENT; Schema: -; Owner: -
+--
+
+COMMENT ON EXTENSION fuzzystrmatch IS 'determine similarities and distance between strings';
+
+
--
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--
@@ -7420,6 +7435,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200520060951'),
('20200803022359'),
('20200816175151'),
-('20201201211748');
+('20201201211748'),
+('20201213052805');
diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb
index 56dead38c..797412d90 100644
--- a/test/functional/tags_controller_test.rb
+++ b/test/functional/tags_controller_test.rb
@@ -58,7 +58,7 @@ class TagsControllerTest < ActionDispatch::IntegrationTest
should respond_to_search(name_matches: "hatsune_miku").with { @miku }
should respond_to_search(name_normalize: "HATSUNE_MIKU ").with { @miku }
should respond_to_search(name_or_alias_matches: "miku").with { @miku }
- should respond_to_search(fuzzy_name_matches: "miku_hatsune", order: "similarity").with { @miku }
+ should respond_to_search(fuzzy_name_matches: "hatsune_mika", order: "similarity").with { @miku }
should respond_to_search(name: "empty", hide_empty: "true").with { [] }
should respond_to_search(name: "empty", hide_empty: "false").with { [@empty] }
From b002bf25f58640e1fe1586546d5202374cab9ca0 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:45:22 -0600
Subject: [PATCH 008/132] autocomplete: display autocorrected tags like
aliases.
Display autocorrected tags similar to aliases, with an arrow pointing at
the corrected tag, but with a dotted underline beneath the misspelled
tag to indicate that it's misspelled.
---
app/javascript/src/javascripts/autocomplete.js.erb | 2 ++
app/javascript/src/styles/common/autocomplete.scss | 4 ++++
app/logical/autocomplete_service.rb | 2 +-
3 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index 1c81f3651..2d4818a95 100644
--- a/app/javascript/src/javascripts/autocomplete.js.erb
+++ b/app/javascript/src/javascripts/autocomplete.js.erb
@@ -182,6 +182,8 @@ Autocomplete.render_item = function(list, item) {
if (item.type === "tag") {
$link.addClass("tag-type-" + item.category);
+ } else if (item.type === "tag_autocorrect") {
+ $link.addClass(`tag-type-${item.category} tag-type-autocorrect`);
} else if (item.type === "user") {
var level_class = "user-" + item.level.toLowerCase();
$link.addClass(level_class);
diff --git a/app/javascript/src/styles/common/autocomplete.scss b/app/javascript/src/styles/common/autocomplete.scss
index 6ca29663e..44158a614 100644
--- a/app/javascript/src/styles/common/autocomplete.scss
+++ b/app/javascript/src/styles/common/autocomplete.scss
@@ -21,4 +21,8 @@
.autocomplete-arrow {
color: var(--autocomplete-arrow-color);
}
+
+ a.tag-type-autocorrect .autocomplete-antecedent {
+ text-decoration: dotted underline;
+ }
}
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 33f5b6757..833c016da 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -103,7 +103,7 @@ class AutocompleteService
tags = Tag.nonempty.autocorrect_matches(string).limit(limit)
tags.map do |tag|
- { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count }
+ { type: "tag_autocorrect", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: string }
end
end
From 71ba45c57ca4e8774d6bdf224809f5cdc34037c1 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 00:52:30 -0600
Subject: [PATCH 009/132] forum: fix /forum_posts?search[linked_to] not
normalizing tags.
Fix searches like https://danbooru.donmai.us/forum_posts?search[linked_to]=touhou%20
not working because the tag wasn't normalized.
---
app/models/forum_post.rb | 6 +++++-
test/functional/forum_posts_controller_test.rb | 1 +
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb
index 62815ff62..2558a33ce 100644
--- a/app/models/forum_post.rb
+++ b/app/models/forum_post.rb
@@ -36,13 +36,17 @@ class ForumPost < ApplicationRecord
where(topic_id: ForumTopic.visible(user))
end
+ def wiki_link_matches(title)
+ where(id: DtextLink.forum_post.wiki_link.where(link_target: WikiPage.normalize_title(title)).select(:model_id))
+ end
+
def search(params)
q = super
q = q.search_attributes(params, :is_deleted, :body)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
if params[:linked_to].present?
- q = q.where(id: DtextLink.forum_post.wiki_link.where(link_target: params[:linked_to]).select(:model_id))
+ q = q.wiki_link_matches(params[:linked_to])
end
q.apply_default_order(params)
diff --git a/test/functional/forum_posts_controller_test.rb b/test/functional/forum_posts_controller_test.rb
index 41cde00d0..be3912546 100644
--- a/test/functional/forum_posts_controller_test.rb
+++ b/test/functional/forum_posts_controller_test.rb
@@ -76,6 +76,7 @@ class ForumPostsControllerTest < ActionDispatch::IntegrationTest
should respond_to_search(body_matches: "xxx").with { @forum_post }
should respond_to_search(body_matches: "bababa").with { [] }
should respond_to_search(is_deleted: "true").with { @unrelated_forum }
+ should respond_to_search(linked_to: "TEST").with { @other_forum }
context "using includes" do
should respond_to_search(topic: {title_matches: "my forum topic"}).with { @forum_post }
From 61e7d32f78d70110965c551b8098f64987961cfd Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 02:12:20 -0600
Subject: [PATCH 010/132] tests: fix FC2 artist normalization url test.
---
test/unit/artist_url_test.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/unit/artist_url_test.rb b/test/unit/artist_url_test.rb
index fc4f22ba4..c9301eecf 100644
--- a/test/unit/artist_url_test.rb
+++ b/test/unit/artist_url_test.rb
@@ -120,11 +120,11 @@ class ArtistUrlTest < ActiveSupport::TestCase
should "normalize fc2 urls" do
url = FactoryBot.create(:artist_url, :url => "http://blog55.fc2.com/monet")
assert_equal("http://blog55.fc2.com/monet", url.url)
- assert_equal("http://blog.fc2.com/monet/", url.normalized_url)
+ assert_equal("http://monet.blog.fc2.com/", url.normalized_url)
url = FactoryBot.create(:artist_url, :url => "http://blog-imgs-55.fc2.com/monet")
assert_equal("http://blog-imgs-55.fc2.com/monet", url.url)
- assert_equal("http://blog.fc2.com/monet/", url.normalized_url)
+ assert_equal("http://monet.blog.fc2.com/", url.normalized_url)
end
should "normalize deviant art artist urls" do
From 8f1d8e2c56e3afbf1f3d003de527546632a84453 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 02:14:57 -0600
Subject: [PATCH 011/132] mailers: fix Rails 6.1 incompatibility.
`add_template_helper` is removed in Rails 6.1.
---
app/mailers/user_mailer.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 787ccf252..b15c195b9 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -1,6 +1,6 @@
class UserMailer < ApplicationMailer
- add_template_helper ApplicationHelper
- add_template_helper UsersHelper
+ helper :application
+ helper :users
def dmail_notice(dmail)
@dmail = dmail
From 62b69eb133b18265bdd1128114ac95cfc8c72966 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 03:38:07 -0600
Subject: [PATCH 012/132] gems: upgrade http-cookie to fix Rails 6.1 bug.
Upgrade the http-cookie gem to a personal fork containing a bugfix for a
http-cookie bug that is triggered by Rails 6.1.
The bug is that HTTP::Cookie objects raise an exception if they're
compared against non-cookie objects. This bug gets triggered when
the Nijie source strategy calls `Rails.cache.fetch` to cache the
Nijie login cookie. `Rails.cache.fetch` ends up calling
ActiveSupport::Cache::Store::Entry#dup_value!, which compares the cookie
with `true`, which triggers the exception.
The http-cookie gem hasn't been updated for 4 years, so we're stuck
patching the library ourselves.
---
Gemfile | 1 +
Gemfile.lock | 10 ++++++++--
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/Gemfile b/Gemfile
index 9f109c6ae..1aef4acc1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -41,6 +41,7 @@ gem 'scenic'
gem 'ipaddress_2'
gem 'http'
gem 'activerecord-hierarchical_query'
+gem 'http-cookie', git: "https://github.com/danbooru/http-cookie"
gem 'pundit'
gem 'mail'
gem 'nokogiri'
diff --git a/Gemfile.lock b/Gemfile.lock
index 66cc5f2ca..e93fe7071 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,10 @@
+GIT
+ remote: https://github.com/danbooru/http-cookie
+ revision: 382d8a641e4df226e0e7b0d2bfaeadb2fe71dd84
+ specs:
+ http-cookie (1.0.4)
+ domain_name (~> 0.5)
+
GIT
remote: https://github.com/evazion/dtext_rb.git
revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7
@@ -161,8 +168,6 @@ GEM
http-cookie (~> 1.0)
http-form_data (~> 2.2)
http-parser (~> 1.2.0)
- http-cookie (1.0.3)
- domain_name (~> 0.5)
http-form_data (2.3.0)
http-parser (1.2.2)
ffi-compiler
@@ -405,6 +410,7 @@ DEPENDENCIES
ffaker
flamegraph
http
+ http-cookie!
ipaddress_2
listen
mail
From 8d87b1a0c0d2df2d3cc80934e8b49deb22e39ba2 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 04:00:32 -0600
Subject: [PATCH 013/132] models: fix deprecated `errors[:base] << "message"`
calls.
Replace the idiom `errors[:base] << "message"` with
`errors.add(:base, "message")`. The former is deprecated in Rails 6.1.
---
app/controllers/emails_controller.rb | 2 +-
app/controllers/passwords_controller.rb | 2 +-
app/controllers/users_controller.rb | 2 +-
app/logical/bulk_update_request_processor.rb | 18 ++++++-------
app/logical/tag_name_validator.rb | 28 ++++++++++----------
app/logical/user_deletion.rb | 4 +--
app/logical/user_name_validator.rb | 10 +++----
app/models/artist.rb | 2 +-
app/models/artist_url.rb | 6 ++---
app/models/ban.rb | 2 +-
app/models/bulk_update_request.rb | 2 +-
app/models/comment_vote.rb | 2 +-
app/models/dmail.rb | 2 +-
app/models/email_address.rb | 2 +-
app/models/favorite_group.rb | 8 +++---
app/models/ip_ban.rb | 14 +++++-----
app/models/note.rb | 4 +--
app/models/pool.rb | 20 +++++++-------
app/models/post.rb | 25 +++++++++--------
app/models/post_appeal.rb | 4 +--
app/models/post_disapproval.rb | 2 +-
app/models/post_flag.rb | 10 +++----
app/models/saved_search.rb | 2 +-
app/models/tag_alias.rb | 2 +-
app/models/tag_implication.rb | 20 +++++++-------
app/models/tag_relationship.rb | 2 +-
app/models/upload.rb | 14 +++++-----
app/models/user_name_change_request.rb | 2 +-
app/models/wiki_page.rb | 4 +--
29 files changed, 108 insertions(+), 109 deletions(-)
diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb
index d9b35a46e..37b385e48 100644
--- a/app/controllers/emails_controller.rb
+++ b/app/controllers/emails_controller.rb
@@ -17,7 +17,7 @@ class EmailsController < ApplicationController
if @user.authenticate_password(params[:user][:password])
@user.update(email_address_attributes: { address: params[:user][:email] })
else
- @user.errors[:base] << "Password was incorrect"
+ @user.errors.add(:base, "Password was incorrect")
end
if @user.errors.none?
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 1a63c5b8a..0e78f9c2e 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -12,7 +12,7 @@ class PasswordsController < ApplicationController
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id])
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
else
- @user.errors[:base] << "Incorrect password"
+ @user.errors.add(:base, "Incorrect password")
end
flash[:notice] = @user.errors.none? ? "Password updated" : @user.errors.full_messages.join("; ")
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 71451d04d..e18888579 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -80,7 +80,7 @@ class UsersController < ApplicationController
flash[:notice] = "Sign up failed"
elsif @user.email_address&.invalid?(:deliverable)
flash[:notice] = "Sign up failed: email address is invalid or doesn't exist"
- @user.errors[:base] << @user.email_address.errors.full_messages.join("; ")
+ @user.errors.add(:base, @user.email_address.errors.full_messages.join("; "))
elsif !@user.save
flash[:notice] = "Sign up failed: #{@user.errors.full_messages.join("; ")}"
else
diff --git a/app/logical/bulk_update_request_processor.rb b/app/logical/bulk_update_request_processor.rb
index 25b96918e..bb4ef8271 100644
--- a/app/logical/bulk_update_request_processor.rb
+++ b/app/logical/bulk_update_request_processor.rb
@@ -55,20 +55,20 @@ class BulkUpdateRequestProcessor
tag_alias = TagAlias.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1])
tag_alias.save(context: validation_context)
if tag_alias.errors.present?
- errors[:base] << "Can't create alias #{tag_alias.antecedent_name} -> #{tag_alias.consequent_name} (#{tag_alias.errors.full_messages.join("; ")})"
+ errors.add(:base, "Can't create alias #{tag_alias.antecedent_name} -> #{tag_alias.consequent_name} (#{tag_alias.errors.full_messages.join("; ")})")
end
when :create_implication
tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], status: "active")
tag_implication.save(context: validation_context)
if tag_implication.errors.present?
- errors[:base] << "Can't create implication #{tag_implication.antecedent_name} -> #{tag_implication.consequent_name} (#{tag_implication.errors.full_messages.join("; ")})"
+ errors.add(:base, "Can't create implication #{tag_implication.antecedent_name} -> #{tag_implication.consequent_name} (#{tag_implication.errors.full_messages.join("; ")})")
end
when :remove_alias
tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_alias.nil?
- errors[:base] << "Can't remove alias #{args[0]} -> #{args[1]} (alias doesn't exist)"
+ errors.add(:base, "Can't remove alias #{args[0]} -> #{args[1]} (alias doesn't exist)")
else
tag_alias.update(status: "deleted")
end
@@ -76,7 +76,7 @@ class BulkUpdateRequestProcessor
when :remove_implication
tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_implication.nil?
- errors[:base] << "Can't remove implication #{args[0]} -> #{args[1]} (implication doesn't exist)"
+ errors.add(:base, "Can't remove implication #{args[0]} -> #{args[1]} (implication doesn't exist)")
else
tag_implication.update(status: "deleted")
end
@@ -84,22 +84,22 @@ class BulkUpdateRequestProcessor
when :change_category
tag = Tag.find_by_name(args[0])
if tag.nil?
- errors[:base] << "Can't change category #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)"
+ errors.add(:base, "Can't change category #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)")
end
when :rename
tag = Tag.find_by_name(args[0])
if tag.nil?
- errors[:base] << "Can't rename #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)"
+ errors.add(:base, "Can't rename #{args[0]} -> #{args[1]} (the '#{args[0]}' tag doesn't exist)")
elsif tag.post_count > MAXIMUM_RENAME_COUNT
- errors[:base] << "Can't rename #{args[0]} -> #{args[1]} ('#{args[0]}' has more than #{MAXIMUM_RENAME_COUNT} posts, use an alias instead)"
+ errors.add(:base, "Can't rename #{args[0]} -> #{args[1]} ('#{args[0]}' has more than #{MAXIMUM_RENAME_COUNT} posts, use an alias instead)")
end
when :mass_update, :nuke
# okay
when :invalid_line
- errors[:base] << "Invalid line: #{args[0]}"
+ errors.add(:base, "Invalid line: #{args[0]}")
else
# should never happen
@@ -113,7 +113,7 @@ class BulkUpdateRequestProcessor
def validate_script_length
if commands.size > MAXIMUM_SCRIPT_LENGTH
- errors[:base] << "Bulk update request is too long (maximum size: #{MAXIMUM_SCRIPT_LENGTH} lines). Split your request into smaller chunks and try again."
+ errors.add(:base, "Bulk update request is too long (maximum size: #{MAXIMUM_SCRIPT_LENGTH} lines). Split your request into smaller chunks and try again.")
end
end
diff --git a/app/logical/tag_name_validator.rb b/app/logical/tag_name_validator.rb
index b4886f33d..10c1d4b67 100644
--- a/app/logical/tag_name_validator.rb
+++ b/app/logical/tag_name_validator.rb
@@ -2,37 +2,37 @@ class TagNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
case Tag.normalize_name(value)
when /\A_*\z/
- record.errors[attribute] << "'#{value}' cannot be blank"
+ record.errors.add(attribute, "'#{value}' cannot be blank")
when /\*/
- record.errors[attribute] << "'#{value}' cannot contain asterisks ('*')"
+ record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
when /,/
- record.errors[attribute] << "'#{value}' cannot contain commas (',')"
+ record.errors.add(attribute, "'#{value}' cannot contain commas (',')")
when /\A~/
- record.errors[attribute] << "'#{value}' cannot begin with a tilde ('~')"
+ record.errors.add(attribute, "'#{value}' cannot begin with a tilde ('~')")
when /\A-/
- record.errors[attribute] << "'#{value}' cannot begin with a dash ('-')"
+ record.errors.add(attribute, "'#{value}' cannot begin with a dash ('-')")
when /\A_/
- record.errors[attribute] << "'#{value}' cannot begin with an underscore"
+ record.errors.add(attribute, "'#{value}' cannot begin with an underscore")
when /_\z/
- record.errors[attribute] << "'#{value}' cannot end with an underscore"
+ record.errors.add(attribute, "'#{value}' cannot end with an underscore")
when /__/
- record.errors[attribute] << "'#{value}' cannot contain consecutive underscores"
+ record.errors.add(attribute, "'#{value}' cannot contain consecutive underscores")
when /[^[:graph:]]/
- record.errors[attribute] << "'#{value}' cannot contain non-printable characters"
+ record.errors.add(attribute, "'#{value}' cannot contain non-printable characters")
when /[^[:ascii:]]/
- record.errors[attribute] << "'#{value}' must consist of only ASCII characters"
+ record.errors.add(attribute, "'#{value}' must consist of only ASCII characters")
when /\A(#{PostQueryBuilder::METATAGS.join("|")}):(.+)\z/i
- record.errors[attribute] << "'#{value}' cannot begin with '#{$1}:'"
+ record.errors.add(attribute, "'#{value}' cannot begin with '#{$1}:'")
when /\A(#{Tag.categories.regexp}):(.+)\z/i
- record.errors[attribute] << "'#{value}' cannot begin with '#{$1}:'"
+ record.errors.add(attribute, "'#{value}' cannot begin with '#{$1}:'")
when "new", "search"
- record.errors[attribute] << "'#{value}' is a reserved name and cannot be used"
+ record.errors.add(attribute, "'#{value}' is a reserved name and cannot be used")
when /\A(.+)_\(cosplay\)\z/i
tag_name = TagAlias.to_aliased([$1]).first
tag = Tag.find_by_name(tag_name)
if tag.present? && !tag.empty? && !tag.character?
- record.errors[attribute] << "#{tag_name} must be a character tag"
+ record.errors.add(attribute, "#{tag_name} must be a character tag")
end
end
end
diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb
index 8577f19a1..0ae9cc3fa 100644
--- a/app/logical/user_deletion.rb
+++ b/app/logical/user_deletion.rb
@@ -61,11 +61,11 @@ class UserDeletion
def validate_deletion
if !user.authenticate_password(password)
- errors[:base] << "Password is incorrect"
+ errors.add(:base, "Password is incorrect")
end
if user.level >= User::Levels::ADMIN
- errors[:base] << "Admins cannot delete their account"
+ errors.add(:base, "Admins cannot delete their account")
end
end
end
diff --git a/app/logical/user_name_validator.rb b/app/logical/user_name_validator.rb
index 93bd4c9d7..2df2decf4 100644
--- a/app/logical/user_name_validator.rb
+++ b/app/logical/user_name_validator.rb
@@ -2,10 +2,10 @@ class UserNameValidator < ActiveModel::EachValidator
def validate_each(rec, attr, value)
name = value
- rec.errors[attr] << "already exists" if User.find_by_name(name).present?
- rec.errors[attr] << "must be 2 to 100 characters long" if !name.length.between?(2, 100)
- rec.errors[attr] << "cannot have whitespace or colons" if name =~ /[[:space:]]|:/
- rec.errors[attr] << "cannot begin or end with an underscore" if name =~ /\A_|_\z/
- rec.errors[attr] << "is not allowed" if name =~ Regexp.union(Danbooru.config.user_name_blacklist)
+ rec.errors.add(attr, "already exists") if User.find_by_name(name).present?
+ rec.errors.add(attr, "must be 2 to 100 characters long") if !name.length.between?(2, 100)
+ rec.errors.add(attr, "cannot have whitespace or colons") if name =~ /[[:space:]]|:/
+ rec.errors.add(attr, "cannot begin or end with an underscore") if name =~ /\A_|_\z/
+ rec.errors.add(attr, "is not allowed") if name =~ Regexp.union(Danbooru.config.user_name_blacklist)
end
end
diff --git a/app/models/artist.rb b/app/models/artist.rb
index abc7260bf..6a2222c65 100644
--- a/app/models/artist.rb
+++ b/app/models/artist.rb
@@ -156,7 +156,7 @@ class Artist < ApplicationRecord
return unless !is_deleted? && name_changed? && tag.present?
if tag.category_name != "Artist" && !tag.empty?
- errors[:base] << "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags"
+ errors.add(:base, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
end
end
diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb
index 625357dac..bab5e04c3 100644
--- a/app/models/artist_url.rb
+++ b/app/models/artist_url.rb
@@ -113,11 +113,11 @@ class ArtistUrl < ApplicationRecord
end
def validate_scheme(uri)
- errors[:url] << "'#{uri}' must begin with http:// or https:// " unless uri.scheme.in?(%w[http https])
+ errors.add(:url, "'#{uri}' must begin with http:// or https:// ") unless uri.scheme.in?(%w[http https])
end
def validate_hostname(uri)
- errors[:url] << "'#{uri}' has a hostname '#{uri.host}' that does not contain a dot" unless uri.host&.include?('.')
+ errors.add(:url, "'#{uri}' has a hostname '#{uri.host}' that does not contain a dot") unless uri.host&.include?('.')
end
def validate_url_format
@@ -125,7 +125,7 @@ class ArtistUrl < ApplicationRecord
validate_scheme(uri)
validate_hostname(uri)
rescue Addressable::URI::InvalidURIError => error
- errors[:url] << "'#{uri}' is malformed: #{error}"
+ errors.add(:url, "'#{uri}' is malformed: #{error}")
end
def self.searchable_includes
diff --git a/app/models/ban.rb b/app/models/ban.rb
index 408a33306..4d91d04ae 100644
--- a/app/models/ban.rb
+++ b/app/models/ban.rb
@@ -45,7 +45,7 @@ class Ban < ApplicationRecord
end
def validate_user_is_bannable
- self.errors[:user] << "is already banned" if user.is_banned?
+ errors.add(:user, "is already banned") if user.is_banned?
end
def update_user_on_create
diff --git a/app/models/bulk_update_request.rb b/app/models/bulk_update_request.rb
index 2149b564f..b42539011 100644
--- a/app/models/bulk_update_request.rb
+++ b/app/models/bulk_update_request.rb
@@ -97,7 +97,7 @@ class BulkUpdateRequest < ApplicationRecord
def validate_script
if processor.invalid?(:request)
- errors[:base] << processor.errors.full_messages.join("; ")
+ errors.add(:base, processor.errors.full_messages.join("; "))
end
end
diff --git a/app/models/comment_vote.rb b/app/models/comment_vote.rb
index ec1a5ce11..e44818f63 100644
--- a/app/models/comment_vote.rb
+++ b/app/models/comment_vote.rb
@@ -26,7 +26,7 @@ class CommentVote < ApplicationRecord
def validate_comment_can_be_down_voted
if is_positive? && comment.creator == CurrentUser.user
- errors.add :base, "You cannot upvote your own comments"
+ errors.add(:base, "You cannot upvote your own comments")
end
end
diff --git a/app/models/dmail.rb b/app/models/dmail.rb
index a789384d9..e3885c236 100644
--- a/app/models/dmail.rb
+++ b/app/models/dmail.rb
@@ -158,7 +158,7 @@ class Dmail < ApplicationRecord
return if from.blank? || from.is_gold?
if from.dmails.where("created_at > ?", 1.hour.ago).group(:to).reorder(nil).count.size >= 10
- errors[:base] << "You can't send dmails to more than 10 users per hour"
+ errors.add(:base, "You can't send dmails to more than 10 users per hour")
end
end
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index 6267ff2bc..367af7a97 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -21,7 +21,7 @@ class EmailAddress < ApplicationRecord
def validate_deliverable
if EmailValidator.undeliverable?(address)
- errors[:address] << "is invalid or does not exist"
+ errors.add(:address, "is invalid or does not exist")
end
end
diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb
index edd4d0dfe..e34ef706b 100644
--- a/app/models/favorite_group.rb
+++ b/app/models/favorite_group.rb
@@ -56,13 +56,13 @@ class FavoriteGroup < ApplicationRecord
if !creator.is_platinum?
error += " Upgrade your account to create more."
end
- self.errors.add(:base, error)
+ errors.add(:base, error)
end
end
def validate_number_of_posts
if post_count > 10_000
- errors[:base] << "Favorite groups can have up to 10,000 posts each"
+ errors.add(:base, "Favorite groups can have up to 10,000 posts each")
end
end
@@ -72,12 +72,12 @@ class FavoriteGroup < ApplicationRecord
nonexisting_post_ids = added_post_ids - existing_post_ids
if nonexisting_post_ids.present?
- errors[:base] << "Cannot add invalid post(s) to favgroup: #{nonexisting_post_ids.to_sentence}"
+ errors.add(:base, "Cannot add invalid post(s) to favgroup: #{nonexisting_post_ids.to_sentence}")
end
duplicate_post_ids = post_ids.group_by(&:itself).transform_values(&:size).select { |id, count| count > 1 }.keys
if duplicate_post_ids.present?
- errors[:base] << "Favgroup already contains post #{duplicate_post_ids.to_sentence}"
+ errors.add(:base, "Favgroup already contains post #{duplicate_post_ids.to_sentence}")
end
end
diff --git a/app/models/ip_ban.rb b/app/models/ip_ban.rb
index 266ff0ea0..76b3b7c05 100644
--- a/app/models/ip_ban.rb
+++ b/app/models/ip_ban.rb
@@ -48,19 +48,19 @@ class IpBan < ApplicationRecord
def validate_ip_addr
if ip_addr.blank?
- errors[:ip_addr] << "is invalid"
+ errors.add(:ip_addr, "is invalid")
elsif ip_addr.private? || ip_addr.loopback? || ip_addr.link_local?
- errors[:ip_addr] << "must be a public address"
+ errors.add(:ip_addr, "must be a public address")
elsif full_ban? && ip_addr.ipv4? && ip_addr.prefix < 24
- errors[:ip_addr] << "may not have a subnet bigger than /24"
+ errors.add(:ip_addr, "may not have a subnet bigger than /24")
elsif partial_ban? && ip_addr.ipv4? && ip_addr.prefix < 8
- errors[:ip_addr] << "may not have a subnet bigger than /8"
+ errors.add(:ip_addr, "may not have a subnet bigger than /8")
elsif full_ban? && ip_addr.ipv6? && ip_addr.prefix < 64
- errors[:ip_addr] << "may not have a subnet bigger than /64"
+ errors.add(:ip_addr, "may not have a subnet bigger than /64")
elsif partial_ban? && ip_addr.ipv6? && ip_addr.prefix < 20
- errors[:ip_addr] << "may not have a subnet bigger than /20"
+ errors.add(:ip_addr, "may not have a subnet bigger than /20")
elsif new_record? && IpBan.active.ip_matches(subnetted_ip).exists?
- errors[:ip_addr] << "is already banned"
+ errors.add(:ip_addr, "is already banned")
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 4facb2028..3cb745da7 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -26,13 +26,13 @@ class Note < ApplicationRecord
extend SearchMethods
def validate_post_is_not_locked
- errors[:post] << "is note locked" if post.is_note_locked?
+ errors.add(:post, "is note locked") if post.is_note_locked?
end
def note_within_image
return false unless post.present?
if x < 0 || y < 0 || (x > post.image_width) || (y > post.image_height) || width < 0 || height < 0 || (x + width > post.image_width) || (y + height > post.image_height)
- self.errors.add(:note, "must be inside the image")
+ errors.add(:note, "must be inside the image")
end
end
diff --git a/app/models/pool.rb b/app/models/pool.rb
index e789f10a7..4c3f59e9d 100644
--- a/app/models/pool.rb
+++ b/app/models/pool.rb
@@ -147,7 +147,7 @@ class Pool < ApplicationRecord
def updater_can_edit_deleted
if is_deleted? && !Pundit.policy!([CurrentUser.user, nil], self).update?
- errors[:base] << "You cannot update pools that are deleted"
+ errors.add(:base, "You cannot update pools that are deleted")
end
end
@@ -254,23 +254,23 @@ class Pool < ApplicationRecord
def validate_name
case name
when /\A(any|none|series|collection)\z/i
- errors[:name] << "cannot be any of the following names: any, none, series, collection"
+ errors.add(:name, "cannot be any of the following names: any, none, series, collection")
when /,/
- errors[:name] << "cannot contain commas"
+ errors.add(:name, "cannot contain commas")
when /\*/
- errors[:name] << "cannot contain asterisks"
+ errors.add(:name, "cannot contain asterisks")
when /\A_/
- errors[:name] << "cannot begin with an underscore"
+ errors.add(:name, "cannot begin with an underscore")
when /_\z/
- errors[:name] << "cannot end with an underscore"
+ errors.add(:name, "cannot end with an underscore")
when /__/
- errors[:name] << "cannot contain consecutive underscores"
+ errors.add(:name, "cannot contain consecutive underscores")
when /[^[:graph:]]/
- errors[:name] << "cannot contain non-printable characters"
+ errors.add(:name, "cannot contain non-printable characters")
when ""
- errors[:name] << "cannot be blank"
+ errors.add(:name, "cannot be blank")
when /\A[0-9]+\z/
- errors[:name] << "cannot contain only digits"
+ errors.add(:name, "cannot contain only digits")
end
end
end
diff --git a/app/models/post.rb b/app/models/post.rb
index 5117102f2..6f54da31c 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -490,7 +490,7 @@ class Post < ApplicationRecord
invalid_tags.each do |tag|
tag.errors.messages.each do |attribute, messages|
- warnings[:base] << "Couldn't add tag: #{messages.join(';')}"
+ warnings.add(:base, "Couldn't add tag: #{messages.join(';')}")
end
end
@@ -976,7 +976,7 @@ class Post < ApplicationRecord
module DeletionMethods
def expunge!
if is_status_locked?
- self.errors.add(:is_status_locked, "; cannot delete post")
+ errors.add(:is_status_locked, "; cannot delete post")
return false
end
@@ -1082,11 +1082,11 @@ class Post < ApplicationRecord
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
transaction do
if id == other_post.id
- errors.add :base, "Source and destination posts are the same"
+ errors.add(:base, "Source and destination posts are the same")
return false
end
unless has_notes?
- errors.add :post, "has no notes"
+ errors.add(:post, "has no notes")
return false
end
@@ -1357,8 +1357,7 @@ class Post < ApplicationRecord
module ValidationMethods
def post_is_not_its_own_parent
if !new_record? && id == parent_id
- errors[:base] << "Post cannot have itself as a parent"
- false
+ errors.add(:base, "Post cannot have itself as a parent")
end
end
@@ -1372,7 +1371,7 @@ class Post < ApplicationRecord
end
def uploader_is_not_limited
- errors[:uploader] << uploader.upload_limit.limit_reason if uploader.upload_limit.limited?
+ errors.add(:uploader, uploader.upload_limit.limit_reason) if uploader.upload_limit.limited?
end
def added_tags_are_valid
@@ -1382,12 +1381,12 @@ class Post < ApplicationRecord
if new_general_tags.present?
n = new_general_tags.size
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
- self.warnings[:base] << "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
+ warnings.add(:base, "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}")
end
new_artist_tags.each do |tag|
if tag.artist.blank?
- self.warnings[:base] << "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]"
+ warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]")
end
end
end
@@ -1398,7 +1397,7 @@ class Post < ApplicationRecord
if unremoved_tags.present?
unremoved_tags_list = unremoved_tags.map { |t| "[[#{t}]]" }.to_sentence
- self.warnings[:base] << "#{unremoved_tags_list} could not be removed. Check for implications and try again"
+ warnings.add(:base, "#{unremoved_tags_list} could not be removed. Check for implications and try again")
end
end
@@ -1409,21 +1408,21 @@ class Post < ApplicationRecord
return if tags.any?(&:artist?)
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null)
- self.warnings[:base] << "Artist tag is required. \"Create new artist tag\":[/artists/new?artist%5Bsource%5D=#{CGI.escape(source)}]. Ask on the forum if you need naming help"
+ warnings.add(:base, "Artist tag is required. \"Create new artist tag\":[/artists/new?artist%5Bsource%5D=#{CGI.escape(source)}]. Ask on the forum if you need naming help")
end
def has_copyright_tag
return if !new_record?
return if has_tag?("copyright_request") || tags.any?(&:copyright?)
- self.warnings[:base] << "Copyright tag is required. Consider adding [[copyright request]] or [[original]]"
+ warnings.add(:base, "Copyright tag is required. Consider adding [[copyright request]] or [[original]]")
end
def has_enough_tags
return if !new_record?
if tags.count(&:general?) < 10
- self.warnings[:base] << "Uploads must have at least 10 general tags. Read [[howto:tag]] for guidelines on tagging your uploads"
+ warnings.add(:base, "Uploads must have at least 10 general tags. Read [[howto:tag]] for guidelines on tagging your uploads")
end
end
end
diff --git a/app/models/post_appeal.rb b/app/models/post_appeal.rb
index 8c8be655a..af54ce997 100644
--- a/app/models/post_appeal.rb
+++ b/app/models/post_appeal.rb
@@ -28,11 +28,11 @@ class PostAppeal < ApplicationRecord
extend SearchMethods
def validate_creator_is_not_limited
- errors[:creator] << "have reached your appeal limit" if creator.is_appeal_limited?
+ errors.add(:creator, "have reached your appeal limit") if creator.is_appeal_limited?
end
def validate_post_is_appealable
- errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable?
+ errors.add(:post, "cannot be appealed") if post.is_status_locked? || !post.is_appealable?
end
def self.searchable_includes
diff --git a/app/models/post_disapproval.rb b/app/models/post_disapproval.rb
index 0c6af91d8..08fac4921 100644
--- a/app/models/post_disapproval.rb
+++ b/app/models/post_disapproval.rb
@@ -51,7 +51,7 @@ class PostDisapproval < ApplicationRecord
def validate_disapproval
if post.is_active?
- errors[:post] << "is already active and cannot be disapproved"
+ errors.add(:post, "is already active and cannot be disapproved")
end
end
diff --git a/app/models/post_flag.rb b/app/models/post_flag.rb
index 80f5b07f1..24493ba2f 100644
--- a/app/models/post_flag.rb
+++ b/app/models/post_flag.rb
@@ -95,17 +95,17 @@ class PostFlag < ApplicationRecord
end
def validate_creator_is_not_limited
- errors[:creator] << "have reached your flag limit" if creator.is_flag_limited? && !is_deletion
+ errors.add(:creator, "have reached your flag limit") if creator.is_flag_limited? && !is_deletion
end
def validate_post
- errors[:post] << "is pending and cannot be flagged" if post.is_pending? && !is_deletion
- errors[:post] << "is deleted and cannot be flagged" if post.is_deleted? && !is_deletion
- errors[:post] << "is locked and cannot be flagged" if post.is_status_locked?
+ errors.add(:post, "is pending and cannot be flagged") if post.is_pending? && !is_deletion
+ errors.add(:post, "is deleted and cannot be flagged") if post.is_deleted? && !is_deletion
+ errors.add(:post, "is locked and cannot be flagged") if post.is_status_locked?
flag = post.flags.in_cooldown.last
if !is_deletion && flag.present?
- errors[:post] << "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
+ errors.add(:post, "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})")
end
end
diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb
index 9036608a9..8ab9aba52 100644
--- a/app/models/saved_search.rb
+++ b/app/models/saved_search.rb
@@ -182,7 +182,7 @@ class SavedSearch < ApplicationRecord
def validate_count
if user.saved_searches.count >= user.max_saved_searches
- self.errors[:user] << "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches)
+ errors.add(:user, "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches))
end
end
diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb
index 6657b3868..9c460e27a 100644
--- a/app/models/tag_alias.rb
+++ b/app/models/tag_alias.rb
@@ -23,7 +23,7 @@ class TagAlias < TagRelationship
tag_alias = TagAlias.active.find_by(antecedent_name: consequent_name)
if tag_alias.present? && tag_alias.consequent_name != antecedent_name
- errors[:base] << "#{tag_alias.antecedent_name} is already aliased to #{tag_alias.consequent_name}"
+ errors.add(:base, "#{tag_alias.antecedent_name} is already aliased to #{tag_alias.consequent_name}")
end
end
diff --git a/app/models/tag_implication.rb b/app/models/tag_implication.rb
index 2564e06f8..df0e4be3a 100644
--- a/app/models/tag_implication.rb
+++ b/app/models/tag_implication.rb
@@ -64,7 +64,7 @@ class TagImplication < TagRelationship
# We don't want a -> b -> a chains
implied_tags = TagImplication.tags_implied_by(consequent_name).map(&:name)
if implied_tags.include?(antecedent_name)
- errors[:base] << "Tag implication can not create a circular relation with another tag implication"
+ errors.add(:base, "Tag implication can not create a circular relation with another tag implication")
end
end
@@ -77,7 +77,7 @@ class TagImplication < TagRelationship
implied_tags = implications.tags_implied_by(antecedent_name).map(&:name)
if implied_tags.include?(consequent_name)
- errors[:base] << "#{antecedent_name} already implies #{consequent_name} through another implication"
+ errors.add(:base, "#{antecedent_name} already implies #{consequent_name} through another implication")
end
end
@@ -86,7 +86,7 @@ class TagImplication < TagRelationship
# We don't want to implicate a -> b if a is already aliased to c
if TagAlias.active.exists?(["antecedent_name = ?", antecedent_name])
- errors[:base] << "Antecedent tag must not be aliased to another tag"
+ errors.add(:base, "Antecedent tag must not be aliased to another tag")
end
end
@@ -95,13 +95,13 @@ class TagImplication < TagRelationship
# We don't want to implicate a -> b if b is already aliased to c
if TagAlias.active.exists?(["antecedent_name = ?", consequent_name])
- errors[:base] << "Consequent tag must not be aliased to another tag"
+ errors.add(:base, "Consequent tag must not be aliased to another tag")
end
end
def tag_categories_are_compatible
if antecedent_tag.category != consequent_tag.category
- errors[:base] << "Can't imply a #{antecedent_tag.category_name.downcase} tag to a #{consequent_tag.category_name.downcase} tag"
+ errors.add(:base, "Can't imply a #{antecedent_tag.category_name.downcase} tag to a #{consequent_tag.category_name.downcase} tag")
end
end
@@ -114,24 +114,24 @@ class TagImplication < TagRelationship
return if antecedent_tag.empty? || consequent_tag.empty?
if antecedent_tag.post_count < MINIMUM_TAG_COUNT
- errors[:base] << "'#{antecedent_name}' must have at least #{MINIMUM_TAG_COUNT} posts"
+ errors.add(:base, "'#{antecedent_name}' must have at least #{MINIMUM_TAG_COUNT} posts")
elsif antecedent_tag.post_count < (MINIMUM_TAG_PERCENTAGE * consequent_tag.post_count)
- errors[:base] << "'#{antecedent_name}' must have at least #{(MINIMUM_TAG_PERCENTAGE * consequent_tag.post_count).to_i} posts"
+ errors.add(:base, "'#{antecedent_name}' must have at least #{(MINIMUM_TAG_PERCENTAGE * consequent_tag.post_count).to_i} posts")
end
max_count = MAXIMUM_TAG_PERCENTAGE * PostQueryBuilder.new("~#{antecedent_name} ~#{consequent_name}").fast_count(timeout: 0).to_i
if antecedent_tag.post_count > max_count && max_count > 0
- errors[:base] << "'#{antecedent_name}' can't make up than #{(MAXIMUM_TAG_PERCENTAGE * 100).to_i}% of '#{consequent_name}'"
+ errors.add(:base, "'#{antecedent_name}' can't make up than #{(MAXIMUM_TAG_PERCENTAGE * 100).to_i}% of '#{consequent_name}'")
end
end
def has_wiki_page
if !antecedent_tag.empty? && antecedent_wiki.blank?
- errors[:base] << "'#{antecedent_name}' must have a wiki page"
+ errors.add(:base, "'#{antecedent_name}' must have a wiki page")
end
if !consequent_tag.empty? && consequent_wiki.blank?
- errors[:base] << "'#{consequent_name}' must have a wiki page"
+ errors.add(:base, "'#{consequent_name}' must have a wiki page")
end
end
end
diff --git a/app/models/tag_relationship.rb b/app/models/tag_relationship.rb
index d6b0f5c73..341f3182d 100644
--- a/app/models/tag_relationship.rb
+++ b/app/models/tag_relationship.rb
@@ -114,7 +114,7 @@ class TagRelationship < ApplicationRecord
def antecedent_and_consequent_are_different
if antecedent_name == consequent_name
- errors[:base] << "Cannot alias or implicate a tag to itself"
+ errors.add(:base, "Cannot alias or implicate a tag to itself")
end
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index e3e8cda1a..04c61a078 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -12,13 +12,13 @@ class Upload < ApplicationRecord
def validate_file_ext(record)
if record.file_ext == "bin"
- record.errors[:file_ext] << "is invalid (only JPEG, PNG, GIF, SWF, MP4, and WebM files are allowed"
+ record.errors.add(:file_ext, "is invalid (only JPEG, PNG, GIF, SWF, MP4, and WebM files are allowed")
end
end
def validate_integrity(record)
if record.media_file.is_corrupt?
- record.errors[:file] << "is corrupted"
+ record.errors.add(:file, "is corrupted")
end
end
@@ -37,24 +37,24 @@ class Upload < ApplicationRecord
return
end
- record.errors[:md5] << "duplicate: #{md5_post.id}"
+ record.errors.add(:md5, "duplicate: #{md5_post.id}")
end
def validate_resolution(record)
resolution = record.image_width.to_i * record.image_height.to_i
if resolution > Danbooru.config.max_image_resolution
- record.errors[:base] << "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)"
+ record.errors.add(:base, "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)")
elsif record.image_width > Danbooru.config.max_image_width
- record.errors[:image_width] << "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})"
+ record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})")
elsif record.image_height > Danbooru.config.max_image_height
- record.errors[:image_height] << "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})"
+ record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})")
end
end
def validate_video_duration(record)
if !record.uploader.is_admin? && record.media_file.is_video? && record.media_file.duration > 120
- record.errors[:base] << "video must not be longer than 2 minutes"
+ record.errors.add(:base, "video must not be longer than 2 minutes")
end
end
end
diff --git a/app/models/user_name_change_request.rb b/app/models/user_name_change_request.rb
index 2cdafe5a6..c3261e000 100644
--- a/app/models/user_name_change_request.rb
+++ b/app/models/user_name_change_request.rb
@@ -30,7 +30,7 @@ class UserNameChangeRequest < ApplicationRecord
def not_limited
if UserNameChangeRequest.unscoped.where(user: user).where("created_at >= ?", 1.week.ago).exists?
- errors[:base] << "You can only submit one name change request per week"
+ errors.add(:base, "You can only submit one name change request per week")
end
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 6f914bd25..c9fd1a7c1 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -116,13 +116,13 @@ class WikiPage < ApplicationRecord
tag_was = Tag.find_by_name(Tag.normalize_name(title_was))
if tag_was.present? && !tag_was.empty?
- warnings[:base] << %!Warning: {{#{title_was}}} still has #{tag_was.post_count} #{"post".pluralize(tag_was.post_count)}. Be sure to move the posts!
+ warnings.add(:base, %!Warning: {{#{title_was}}} still has #{tag_was.post_count} #{"post".pluralize(tag_was.post_count)}. Be sure to move the posts!)
end
broken_wikis = WikiPage.linked_to(title_was)
if broken_wikis.count > 0
broken_wiki_search = Rails.application.routes.url_helpers.wiki_pages_path(search: { linked_to: title_was })
- warnings[:base] << %!Warning: [[#{title_was}]] is still linked from "#{broken_wikis.count} #{"other wiki page".pluralize(broken_wikis.count)}":[#{broken_wiki_search}]. Update #{(broken_wikis.count > 1) ? "these wikis" : "this wiki"} to link to [[#{title}]] instead!
+ warnings.add(:base, %!Warning: [[#{title_was}]] is still linked from "#{broken_wikis.count} #{"other wiki page".pluralize(broken_wikis.count)}":[#{broken_wiki_search}]. Update #{(broken_wikis.count > 1) ? "these wikis" : "this wiki"} to link to [[#{title}]] instead!)
end
end
From 35134abe8fe7a29051efcd692a1be27e8e9f13ab Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 04:02:25 -0600
Subject: [PATCH 014/132] post query builder: fix incompatibilities with Rails
6.1.
* Rename the `#negate` and `#and` methods that we monkey patch into
ActiveRecord::Relation. These methods are now defined in Rails 6.1, but
they shadow our methods and have slightly different behavior.
* Fix a call to `invert`. It no longer accepts an argument.
---
app/logical/concerns/searchable.rb | 7 ++++---
app/logical/post_query_builder.rb | 4 ++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index 151ad5f86..3cde3b5ec 100644
--- a/app/logical/concerns/searchable.rb
+++ b/app/logical/concerns/searchable.rb
@@ -10,12 +10,13 @@ module Searchable
1 + params.values.map { |v| parameter_hash?(v) ? parameter_depth(v) : 1 }.max
end
- def negate(kind = :nand)
- unscoped.where(all.where_clause.invert(kind).ast)
+ def negate_relation
+ unscoped.where(all.where_clause.invert.ast)
end
# XXX hacky method to AND two relations together.
- def and(relation)
+ # XXX Replace with ActiveRecord#and (cf https://github.com/rails/rails/pull/39328)
+ def and_relation(relation)
q = all
q = q.where(relation.where_clause.ast) if relation.where_clause.present?
q = q.joins(relation.joins_values + q.joins_values) if relation.joins_values.present?
diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb
index 19c67138e..4046485f1 100644
--- a/app/logical/post_query_builder.rb
+++ b/app/logical/post_query_builder.rb
@@ -92,8 +92,8 @@ class PostQueryBuilder
def metatags_match(metatags, relation)
metatags.each do |metatag|
clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted)
- clause = clause.negate if metatag.negated
- relation = relation.and(clause)
+ clause = clause.negate_relation if metatag.negated
+ relation = relation.and_relation(clause)
end
relation
From b3ad13e6e3f61c1f3fd70f72035ebbb754dfa279 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 14:55:49 -0600
Subject: [PATCH 015/132] users: add new owner level.
Add a new Owner user level for the site owner. Highly sensitive
operations like manually changing the passwords of other users will be
restricted to the site owner.
---
app/javascript/src/styles/common/user_styles.scss | 4 ++++
.../src/styles/specific/user_tooltips.scss | 1 +
app/logical/user_deletion.rb | 2 +-
app/models/user.rb | 15 ++++++++++++++-
test/factories/user.rb | 5 +++++
test/mailers/previews/user_mailer_preview.rb | 2 +-
test/unit/user_test.rb | 7 +++++++
7 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/app/javascript/src/styles/common/user_styles.scss b/app/javascript/src/styles/common/user_styles.scss
index 52cc6453f..14aff9a51 100644
--- a/app/javascript/src/styles/common/user_styles.scss
+++ b/app/javascript/src/styles/common/user_styles.scss
@@ -1,4 +1,8 @@
body[data-current-user-style-usernames="true"] {
+ a.user-owner {
+ color: var(--user-admin-color);
+ }
+
a.user-admin {
color: var(--user-admin-color);
}
diff --git a/app/javascript/src/styles/specific/user_tooltips.scss b/app/javascript/src/styles/specific/user_tooltips.scss
index b7133ef31..881dae1d6 100644
--- a/app/javascript/src/styles/specific/user_tooltips.scss
+++ b/app/javascript/src/styles/specific/user_tooltips.scss
@@ -27,6 +27,7 @@
margin-right: 0.25em;
border-radius: 3px;
+ &.user-tooltip-badge-owner { background-color: var(--user-admin-color); }
&.user-tooltip-badge-admin { background-color: var(--user-admin-color); }
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); }
diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb
index 0ae9cc3fa..e39192736 100644
--- a/app/logical/user_deletion.rb
+++ b/app/logical/user_deletion.rb
@@ -64,7 +64,7 @@ class UserDeletion
errors.add(:base, "Password is incorrect")
end
- if user.level >= User::Levels::ADMIN
+ if user.is_admin?
errors.add(:base, "Admins cannot delete their account")
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5ec821bcb..73e2cb116 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -12,6 +12,7 @@ class User < ApplicationRecord
BUILDER = 32
MODERATOR = 40
ADMIN = 50
+ OWNER = 60
end
# Used for `before_action :_only`. Must have a corresponding `is_?` method.
@@ -191,6 +192,10 @@ class User < ApplicationRecord
extend ActiveSupport::Concern
module ClassMethods
+ def owner
+ User.find_by!(level: Levels::ADMIN)
+ end
+
def system
User.find_by!(name: Danbooru.config.system_user)
end
@@ -208,7 +213,8 @@ class User < ApplicationRecord
"Platinum" => Levels::PLATINUM,
"Builder" => Levels::BUILDER,
"Moderator" => Levels::MODERATOR,
- "Admin" => Levels::ADMIN
+ "Admin" => Levels::ADMIN,
+ "Owner" => Levels::OWNER
}
end
@@ -235,6 +241,9 @@ class User < ApplicationRecord
when Levels::ADMIN
"Admin"
+ when Levels::OWNER
+ "Owner"
+
else
""
end
@@ -299,6 +308,10 @@ class User < ApplicationRecord
level >= Levels::ADMIN
end
+ def is_owner?
+ level >= Levels::OWNER
+ end
+
def is_approver?
can_approve_posts?
end
diff --git a/test/factories/user.rb b/test/factories/user.rb
index 577334f40..e06e2c29c 100644
--- a/test/factories/user.rb
+++ b/test/factories/user.rb
@@ -56,6 +56,11 @@ FactoryBot.define do
can_approve_posts {true}
end
+ factory(:owner_user) do
+ level { User::Levels::OWNER }
+ can_approve_posts {true}
+ end
+
factory(:uploader) do
created_at { 2.weeks.ago }
end
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb
index 3c466e034..314267a9c 100644
--- a/test/mailers/previews/user_mailer_preview.rb
+++ b/test/mailers/previews/user_mailer_preview.rb
@@ -1,6 +1,6 @@
class UserMailerPreview < ActionMailer::Preview
def dmail_notice
- dmail = User.admins.first.dmails.first
+ dmail = User.system.dmails.first
UserMailer.dmail_notice(dmail)
end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index 36d34af45..012d1779b 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -45,7 +45,14 @@ class UserTest < ActiveSupport::TestCase
end
should "normalize its level" do
+ user = FactoryBot.create(:user, :level => User::Levels::OWNER)
+ assert(user.is_owner?)
+ assert(user.is_admin?)
+ assert(user.is_moderator?)
+ assert(user.is_gold?)
+
user = FactoryBot.create(:user, :level => User::Levels::ADMIN)
+ assert(!user.is_owner?)
assert(user.is_moderator?)
assert(user.is_gold?)
From c82e05d8280d1fc590dbcaf57d62a37499fcbef6 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 16:54:45 -0600
Subject: [PATCH 016/132] users: add stricter checks for user promotions.
New rules for user promotions:
* Moderators can no longer promote other users to moderator level. Only
Admins can promote users to Mod level. Mods can only promote up to Builder level.
* Admins can no longer promote other users to Admin level. Only Owners
can promote users to Admin. Admins can only promote up to Mod level.
* Admins can no longer demote themselves or other admins.
These rules are being changed to account for the new Owner user level.
Also change it so that when a user upgrades their account, the promotion
is done by DanbooruBot. This means that the inviter and the mod action
will show DanbooruBot as the promoter instead of the user themselves.
---
app/controllers/admin/users_controller.rb | 8 +-
app/controllers/user_upgrades_controller.rb | 2 +-
app/logical/user_promotion.rb | 50 +++++------
app/models/user.rb | 4 +-
db/populate.rb | 2 +-
.../functional/admin/users_controller_test.rb | 18 ++++
test/unit/user_test.rb | 85 +++++++++++++++++--
7 files changed, 131 insertions(+), 38 deletions(-)
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1422bd3de..0337c892a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -6,7 +6,13 @@ module Admin
def update
@user = authorize User.find(params[:id]), :promote?
- @user.promote_to!(params[:user][:level], params[:user])
+
+ @level = params.dig(:user, :level)
+ @can_upload_free = params.dig(:user, :can_upload_free)
+ @can_approve_posts = params.dig(:user, :can_approve_posts)
+
+ @user.promote_to!(@level, CurrentUser.user, can_upload_free: @can_upload_free, can_approve_posts: @can_approve_posts)
+
redirect_to edit_admin_user_path(@user), :notice => "User updated"
end
end
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index 8001b1d88..903f7b6ec 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -48,7 +48,7 @@ class UserUpgradesController < ApplicationController
:card => params[:stripeToken],
:description => params[:desc]
)
- @user.promote_to!(level, is_upgrade: true)
+ @user.promote_to!(level, User.system, is_upgrade: true)
flash[:success] = true
rescue Stripe::CardError => e
DanbooruLogger.log(e)
diff --git a/app/logical/user_promotion.rb b/app/logical/user_promotion.rb
index b3b7a32bb..457c67cce 100644
--- a/app/logical/user_promotion.rb
+++ b/app/logical/user_promotion.rb
@@ -1,33 +1,28 @@
class UserPromotion
- attr_reader :user, :promoter, :new_level, :options, :old_can_approve_posts, :old_can_upload_free
+ attr_reader :user, :promoter, :new_level, :old_can_approve_posts, :old_can_upload_free, :can_upload_free, :can_approve_posts, :is_upgrade
- def initialize(user, promoter, new_level, options = {})
+ def initialize(user, promoter, new_level, can_upload_free: nil, can_approve_posts: nil, is_upgrade: false)
@user = user
@promoter = promoter
- @new_level = new_level
- @options = options
+ @new_level = new_level.to_i
+ @can_upload_free = can_upload_free
+ @can_approve_posts = can_approve_posts
+ @is_upgrade = is_upgrade
end
def promote!
- validate
+ validate!
@old_can_approve_posts = user.can_approve_posts?
@old_can_upload_free = user.can_upload_free?
user.level = new_level
+ user.can_upload_free = can_upload_free unless can_upload_free.nil?
+ user.can_approve_posts = can_approve_posts unless can_approve_posts.nil?
+ user.inviter = promoter
- if options.key?(:can_approve_posts)
- user.can_approve_posts = options[:can_approve_posts]
- end
-
- if options.key?(:can_upload_free)
- user.can_upload_free = options[:can_upload_free]
- end
-
- user.inviter_id = promoter.id
-
- create_user_feedback unless options[:is_upgrade]
- create_dmail unless options[:skip_dmail]
+ create_user_feedback unless is_upgrade
+ create_dmail
create_mod_actions
user.save
@@ -45,20 +40,21 @@ class UserPromotion
end
if user.level_changed?
- category = options[:is_upgrade] ? :user_account_upgrade : :user_level_change
+ category = is_upgrade ? :user_account_upgrade : :user_level_change
ModAction.log(%{"#{user.name}":/users/#{user.id} level changed #{user.level_string_was} -> #{user.level_string}}, category)
end
end
- def validate
- # admins can do anything
- return if promoter.is_admin?
-
- # can't promote/demote moderators
- raise User::PrivilegeError if user.is_moderator?
-
- # can't promote to admin
- raise User::PrivilegeError if new_level.to_i >= User::Levels::ADMIN
+ def validate!
+ if !promoter.is_moderator?
+ raise User::PrivilegeError, "You can't promote or demote other users"
+ elsif promoter == user
+ raise User::PrivilegeError, "You can't promote or demote yourself"
+ elsif new_level >= promoter.level
+ raise User::PrivilegeError, "You can't promote other users to your rank or above"
+ elsif user.level >= promoter.level
+ raise User::PrivilegeError, "You can't promote or demote other users at your rank or above"
+ end
end
def build_messages
diff --git a/app/models/user.rb b/app/models/user.rb
index 73e2cb116..ec14ed950 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -250,8 +250,8 @@ class User < ApplicationRecord
end
end
- def promote_to!(new_level, options = {})
- UserPromotion.new(self, CurrentUser.user, new_level, options).promote!
+ def promote_to!(new_level, promoter = CurrentUser.user, **options)
+ UserPromotion.new(self, promoter, new_level, **options).promote!
end
def promote_to_admin_if_first_user
diff --git a/db/populate.rb b/db/populate.rb
index c6451e7b7..7a66a7080 100644
--- a/db/populate.rb
+++ b/db/populate.rb
@@ -53,7 +53,7 @@ if User.count == 0
password: "password1",
password_confirmation: "password1"
)
- newuser.promote_to!(User::Levels.const_get(level), :is_upgrade => true, :skip_dmail => true)
+ newuser.promote_to!(User::Levels.const_get(level), user)
end
newuser = User.create(
diff --git a/test/functional/admin/users_controller_test.rb b/test/functional/admin/users_controller_test.rb
index 2cc47a0fe..c56470544 100644
--- a/test/functional/admin/users_controller_test.rb
+++ b/test/functional/admin/users_controller_test.rb
@@ -24,6 +24,24 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(@mod.id, @user.inviter_id)
end
+ should "promote the user to unrestricted uploads" do
+ put_auth admin_user_path(@user), @mod, params: { user: { level: User::Levels::BUILDER, can_upload_free: true }}
+
+ assert_redirected_to(edit_admin_user_path(@user.reload))
+ assert_equal(true, @user.is_builder?)
+ assert_equal(true, @user.can_upload_free?)
+ assert_equal(false, @user.can_approve_posts?)
+ end
+
+ should "promote the user to approver" do
+ put_auth admin_user_path(@user), @mod, params: { user: { level: User::Levels::BUILDER, can_approve_posts: true }}
+
+ assert_redirected_to(edit_admin_user_path(@user.reload))
+ assert_equal(true, @user.is_builder?)
+ assert_equal(false, @user.can_upload_free?)
+ assert_equal(true, @user.can_approve_posts?)
+ end
+
context "promoted to an admin" do
should "fail" do
put_auth admin_user_path(@user), @mod, params: {:user => {:level => "50"}}
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index 012d1779b..cfa83b76a 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -1,6 +1,19 @@
require 'test_helper'
class UserTest < ActiveSupport::TestCase
+ def assert_promoted_to(new_level, user, promoter)
+ user.promote_to!(new_level, promoter)
+ assert_equal(new_level, user.reload.level)
+ end
+
+ def assert_not_promoted_to(new_level, user, promoter)
+ assert_raise(User::PrivilegeError) do
+ user.promote_to!(new_level, promoter)
+ end
+
+ assert_not_equal(new_level, user.reload.level)
+ end
+
context "A user" do
setup do
@user = FactoryBot.create(:user)
@@ -15,14 +28,74 @@ class UserTest < ActiveSupport::TestCase
context "promoting a user" do
setup do
- CurrentUser.user = FactoryBot.create(:moderator_user)
+ @builder = create(:builder_user)
+ @mod = create(:moderator_user)
+ @admin = create(:admin_user)
+ @owner = create(:owner_user)
+ end
+
+ should "allow moderators to promote users up to builder level" do
+ assert_promoted_to(User::Levels::GOLD, @user, @mod)
+ assert_promoted_to(User::Levels::PLATINUM, @user, @mod)
+ assert_promoted_to(User::Levels::BUILDER, @user, @mod)
+
+ assert_not_promoted_to(User::Levels::MODERATOR, @user, @mod)
+ assert_not_promoted_to(User::Levels::ADMIN, @user, @mod)
+ assert_not_promoted_to(User::Levels::OWNER, @user, @mod)
+ end
+
+ should "allow admins to promote users up to moderator level" do
+ assert_promoted_to(User::Levels::GOLD, @user, @admin)
+ assert_promoted_to(User::Levels::PLATINUM, @user, @admin)
+ assert_promoted_to(User::Levels::BUILDER, @user, @admin)
+ assert_promoted_to(User::Levels::MODERATOR, @user, @admin)
+
+ assert_not_promoted_to(User::Levels::ADMIN, @user, @admin)
+ assert_not_promoted_to(User::Levels::OWNER, @user, @admin)
+ end
+
+ should "allow the owner to promote users up to admin level" do
+ assert_promoted_to(User::Levels::GOLD, @user, @owner)
+ assert_promoted_to(User::Levels::PLATINUM, @user, @owner)
+ assert_promoted_to(User::Levels::BUILDER, @user, @owner)
+ assert_promoted_to(User::Levels::MODERATOR, @user, @owner)
+ assert_promoted_to(User::Levels::ADMIN, @user, @owner)
+
+ assert_not_promoted_to(User::Levels::OWNER, @user, @owner)
+ end
+
+ should "not allow non-moderators to promote other users" do
+ assert_not_promoted_to(User::Levels::GOLD, @user, @builder)
+ assert_not_promoted_to(User::Levels::PLATINUM, @user, @builder)
+ assert_not_promoted_to(User::Levels::BUILDER, @user, @builder)
+ assert_not_promoted_to(User::Levels::MODERATOR, @user, @builder)
+ assert_not_promoted_to(User::Levels::ADMIN, @user, @builder)
+ assert_not_promoted_to(User::Levels::OWNER, @user, @builder)
+ end
+
+ should "not allow users to promote or demote other users at their rank or above" do
+ assert_not_promoted_to(User::Levels::ADMIN, create(:moderator_user), @mod)
+ assert_not_promoted_to(User::Levels::BUILDER, create(:moderator_user), @mod)
+
+ assert_not_promoted_to(User::Levels::OWNER, create(:admin_user), @admin)
+ assert_not_promoted_to(User::Levels::MODERATOR, create(:admin_user), @admin)
+
+ assert_not_promoted_to(User::Levels::ADMIN, create(:owner_user), @owner)
+ end
+
+ should "not allow users to promote themselves" do
+ assert_not_promoted_to(User::Levels::ADMIN, @mod, @mod)
+ assert_not_promoted_to(User::Levels::OWNER, @admin, @admin)
+ end
+
+ should "not allow users to demote themselves" do
+ assert_not_promoted_to(User::Levels::MEMBER, @mod, @mod)
+ assert_not_promoted_to(User::Levels::MEMBER, @admin, @admin)
+ assert_not_promoted_to(User::Levels::MEMBER, @owner, @owner)
end
should "create a neutral feedback" do
- assert_difference("UserFeedback.count") do
- @user.promote_to!(User::Levels::GOLD)
- end
-
+ @user.promote_to!(User::Levels::GOLD, @mod)
assert_equal("You have been promoted to a Gold level account from Member.", @user.feedback.last.body)
end
@@ -31,7 +104,7 @@ class UserTest < ActiveSupport::TestCase
User.stubs(:system).returns(bot)
assert_difference("Dmail.count", 1) do
- @user.promote_to!(User::Levels::GOLD)
+ @user.promote_to!(User::Levels::GOLD, @admin)
end
assert(@user.dmails.exists?(from: bot, to: @user, title: "Your account has been updated"))
From 2144f45fa462390bcde336ecc908d8a92149430c Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 18:33:46 -0600
Subject: [PATCH 017/132] users: add account upgrade integration tests.
* Test that the user upgrade process integrates with Stripe correctly.
* Replace a deprecated `card` param with `source` in `Stripe::Charge.create`.
* Rescue Stripe::StripeError instead of Stripe::CardError so that we
handle failures outside of card failures, such as network errors.
---
Gemfile | 1 +
Gemfile.lock | 6 ++
app/controllers/user_upgrades_controller.rb | 9 +-
.../user_upgrades_controller_test.rb | 100 ++++++++++++++++++
4 files changed, 109 insertions(+), 7 deletions(-)
diff --git a/Gemfile b/Gemfile
index 1aef4acc1..92332c2e6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -86,4 +86,5 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
gem "codecov", require: false
+ gem 'stripe-ruby-mock', require: "stripe_mock"
end
diff --git a/Gemfile.lock b/Gemfile.lock
index e93fe7071..1bab4040e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -134,6 +134,7 @@ GEM
concurrent-ruby (1.1.7)
crass (1.0.6)
daemons (1.3.1)
+ dante (0.2.0)
delayed_job (4.1.8)
activesupport (>= 3.0, < 6.1)
delayed_job_active_record (4.1.4)
@@ -354,6 +355,10 @@ GEM
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
stripe (5.28.0)
+ stripe-ruby-mock (3.0.1)
+ dante (>= 0.2.0)
+ multi_json (~> 1.0)
+ stripe (> 5, < 6)
thor (1.0.1)
thread_safe (0.3.6)
tzinfo (1.2.8)
@@ -452,6 +457,7 @@ DEPENDENCIES
stackprof
streamio-ffmpeg
stripe
+ stripe-ruby-mock
unicorn
unicorn-worker-killer
webpacker (>= 4.0.x)
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index 903f7b6ec..be7ee116a 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -42,15 +42,10 @@ class UserUpgradesController < ApplicationController
end
begin
- charge = Stripe::Charge.create(
- :amount => cost,
- :currency => "usd",
- :card => params[:stripeToken],
- :description => params[:desc]
- )
+ charge = Stripe::Charge.create(amount: cost, currency: "usd", source: params[:stripeToken], description: params[:desc])
@user.promote_to!(level, User.system, is_upgrade: true)
flash[:success] = true
- rescue Stripe::CardError => e
+ rescue Stripe::StripeError => e
DanbooruLogger.log(e)
flash[:error] = e.message
end
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index b4450f8a1..8fbd64d34 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -1,6 +1,14 @@
require 'test_helper'
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ StripeMock.start
+ end
+
+ teardown do
+ StripeMock.stop
+ end
+
context "The user upgrades controller" do
context "new action" do
should "render" do
@@ -15,5 +23,97 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
end
+
+ context "create action" do
+ setup do
+ @user = create(:user)
+ @token = StripeMock.generate_card_token
+ end
+
+ context "a self upgrade" do
+ should "upgrade a Member to Gold" do
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_gold?)
+ end
+
+ should "upgrade a Member to Platinum" do
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Platinum" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_platinum?)
+ end
+
+ should "upgrade a Gold user to Platinum" do
+ @user.update!(level: User::Levels::GOLD)
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade Gold to Platinum" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_platinum?)
+ end
+
+ should "log an account upgrade modaction" do
+ assert_difference("ModAction.user_account_upgrade.count") do
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
+ end
+ end
+
+ should "send the user a dmail" do
+ assert_difference("@user.dmails.received.count") do
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
+ end
+ end
+ end
+
+ context "a gifted upgrade" do
+ should "upgrade the user to Gold" do
+ @other_user = create(:user)
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @other_user.id }
+
+ assert_redirected_to user_upgrade_path(user_id: @other_user.id)
+ assert_equal(true, @other_user.reload.is_gold?)
+ assert_equal(false, @user.reload.is_gold?)
+ end
+ end
+
+ context "an upgrade with a missing Stripe token" do
+ should "not upgrade the user" do
+ post_auth user_upgrade_path, @user, params: { desc: "Upgrade to Gold" }
+
+ assert_response :success
+ assert_equal(true, @user.reload.is_member?)
+ end
+ end
+
+ context "an upgrade with an invalid Stripe token" do
+ should "not upgrade the user" do
+ post_auth user_upgrade_path, @user, params: { stripeToken: "garbage", desc: "Upgrade to Gold" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_member?)
+ end
+ end
+
+ context "an upgrade with an credit card that is declined" do
+ should "not upgrade the user" do
+ StripeMock.prepare_card_error(:card_declined)
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_member?)
+ end
+ end
+
+ context "an upgrade with an credit card that is expired" do
+ should "not upgrade the user" do
+ StripeMock.prepare_card_error(:expired_card)
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
+
+ assert_redirected_to user_upgrade_path
+ assert_equal(true, @user.reload.is_member?)
+ end
+ end
+ end
end
end
From d8b51e3f022b28bf1bcc594fcd1c5f1aca79724d Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 18:43:34 -0600
Subject: [PATCH 018/132] users: don't allow gifting upgrades to demote
privileged users.
Don't allow gifting Gold or Platinum upgrades to users above Platinum
level. Fixes an exploit where you could demote Builders and above by
gifting them an upgrade.
---
app/logical/user_promotion.rb | 2 ++
test/functional/user_upgrades_controller_test.rb | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/app/logical/user_promotion.rb b/app/logical/user_promotion.rb
index 457c67cce..e9653bb2d 100644
--- a/app/logical/user_promotion.rb
+++ b/app/logical/user_promotion.rb
@@ -54,6 +54,8 @@ class UserPromotion
raise User::PrivilegeError, "You can't promote other users to your rank or above"
elsif user.level >= promoter.level
raise User::PrivilegeError, "You can't promote or demote other users at your rank or above"
+ elsif is_upgrade && user.is_builder?
+ raise User::PrivilegeError, "You can't upgrade a user that is above Platinum level"
end
end
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index 8fbd64d34..db88aa327 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -77,6 +77,16 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
end
end
+ context "an upgrade for a user above Platinum level" do
+ should "not demote the user" do
+ @builder = create(:builder_user)
+ post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @builder.id }
+
+ assert_response 403
+ assert_equal(true, @builder.reload.is_builder?)
+ end
+ end
+
context "an upgrade with a missing Stripe token" do
should "not upgrade the user" do
post_auth user_upgrade_path, @user, params: { desc: "Upgrade to Gold" }
From 86bba56eda6b1757e67e67778130f1e5b5d3edda Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 19:07:19 -0600
Subject: [PATCH 019/132] users: allow site owner to reset passwords of other
users.
---
app/controllers/passwords_controller.rb | 2 +-
app/policies/password_policy.rb | 2 +-
test/functional/passwords_controller_test.rb | 18 ++++++++++++++++++
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 0e78f9c2e..f5fcc40e0 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -9,7 +9,7 @@ class PasswordsController < ApplicationController
def update
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
- if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id])
+ if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id]) || CurrentUser.user.is_owner?
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
else
@user.errors.add(:base, "Incorrect password")
diff --git a/app/policies/password_policy.rb b/app/policies/password_policy.rb
index a315c507e..3c34af574 100644
--- a/app/policies/password_policy.rb
+++ b/app/policies/password_policy.rb
@@ -1,5 +1,5 @@
class PasswordPolicy < ApplicationPolicy
def update?
- record.id == user.id || user.is_admin?
+ record.id == user.id || user.is_owner?
end
end
diff --git a/test/functional/passwords_controller_test.rb b/test/functional/passwords_controller_test.rb
index aae7c9acb..f865957f2 100644
--- a/test/functional/passwords_controller_test.rb
+++ b/test/functional/passwords_controller_test.rb
@@ -31,6 +31,24 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_equal(@user, @user.authenticate_password("abcde"))
end
+ should "allow the site owner to change the password of other users" do
+ @owner = create(:owner_user)
+ put_auth user_password_path(@user), @owner, params: { user: { password: "abcde", password_confirmation: "abcde" } }
+
+ assert_redirected_to @user
+ assert_equal(false, @user.reload.authenticate_password("12345"))
+ assert_equal(@user, @user.authenticate_password("abcde"))
+ end
+
+ should "not allow non-owners to change the password of other users" do
+ @admin = create(:admin_user)
+ put_auth user_password_path(@user), @admin, params: { user: { old_password: "12345", password: "abcde", password_confirmation: "abcde" } }
+
+ assert_response 403
+ assert_equal(@user, @user.reload.authenticate_password("12345"))
+ assert_equal(false, @user.authenticate_password("abcde"))
+ end
+
should "not update the password when given an invalid old password" do
put_auth user_password_path(@user), @user, params: { user: { old_password: "3qoirjqe", password: "abcde", password_confirmation: "abcde" } }
From 9f09c495e46b62a780d61daacd775087ac0c83f1 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 19:09:49 -0600
Subject: [PATCH 020/132] users: don't allow admins to edit user levels
directly.
Don't allow admins to bypass promotion restrictions by manually updating
user levels with a `PUT /users/:id` API call. Level changes have to go
through the /admin/users/:id/edit page.
---
app/policies/user_policy.rb | 1 -
test/functional/users_controller_test.rb | 6 +++---
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 0c9c0469c..6ca305635 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -47,7 +47,6 @@ class UserPolicy < ApplicationPolicy
:disable_tagged_filenames, :disable_cropped_thumbnails,
:disable_mobile_gestures, :enable_safe_mode, :enable_desktop_mode,
:disable_post_tooltips,
- (:level if CurrentUser.is_admin?)
].compact
end
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index c6c02d522..7d35e591f 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -335,11 +335,11 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
context "changing the level" do
should "not work" do
- @cuser = create(:user)
- put_auth user_path(@user), @cuser, params: {:user => {:level => 40}}
+ @owner = create(:owner_user)
+ put_auth user_path(@user), @owner, params: { user: { level: User::Levels::BUILDER }}
assert_response 403
- assert_equal(20, @user.reload.level)
+ assert_equal(User::Levels::MEMBER, @user.reload.level)
end
end
From 67eefadd7f6528b10c4f973bee7d68cd183537df Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 19:50:25 -0600
Subject: [PATCH 021/132] users: let mods see email addresses on user profiles.
* Let Mods and Admins see the email addresses of users below their level.
* Let users see their own email address on their profile.
* Let users verify or edit their email address from their profile.
This is to make catching sockpuppets easier, and to make it easier for
users to fix their email.
---
app/javascript/src/styles/base/040_colors.css | 6 ++++
app/javascript/src/styles/specific/users.scss | 8 +++++
app/policies/email_address_policy.rb | 2 +-
app/policies/nil_class_policy.rb | 21 ++++++++++++++
app/views/emails/verify.html.erb | 8 ++---
app/views/users/_statistics.html.erb | 28 ++++++++++++++++++
test/functional/users_controller_test.rb | 29 ++++++++++++++++++-
7 files changed, 96 insertions(+), 6 deletions(-)
create mode 100644 app/policies/nil_class_policy.rb
diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css
index 28eef86ec..5dc30e914 100644
--- a/app/javascript/src/styles/base/040_colors.css
+++ b/app/javascript/src/styles/base/040_colors.css
@@ -201,6 +201,9 @@
--user-member-color: var(--link-color);
--user-banned-color: black;
+ --user-verified-email-color: #0A0;
+ --user-unverified-email-color: #F80;
+
--news-updates-background: #EEE;
--news-updates-border: 2px solid #666;
@@ -291,6 +294,9 @@ body[data-current-user-theme="dark"] {
--user-moderator-color: var(--green-1);
--user-admin-color: var(--red-1);
+ --user-verified-email-color: var(--green-1);
+ --user-unverified-email-color: var(--yellow-1);
+
/* misc specific colors */
--autocomplete-selected-background-color: var(--grey-3);
--autocomplete-border: 1px solid var(--grey-4);
diff --git a/app/javascript/src/styles/specific/users.scss b/app/javascript/src/styles/specific/users.scss
index 708538025..ba9af319f 100644
--- a/app/javascript/src/styles/specific/users.scss
+++ b/app/javascript/src/styles/specific/users.scss
@@ -30,6 +30,14 @@ div#c-users {
p {
margin-bottom: 0.5em;
}
+
+ .user-verified-email-icon {
+ color: var(--user-verified-email-color);
+ }
+
+ .user-unverified-email-icon {
+ color: var(--user-unverified-email-color);
+ }
}
}
diff --git a/app/policies/email_address_policy.rb b/app/policies/email_address_policy.rb
index cd92232d1..152469125 100644
--- a/app/policies/email_address_policy.rb
+++ b/app/policies/email_address_policy.rb
@@ -1,6 +1,6 @@
class EmailAddressPolicy < ApplicationPolicy
def show?
- record.user_id == user.id
+ record.user_id == user.id || (user.is_moderator? && record.user.level < user.level)
end
def update?
diff --git a/app/policies/nil_class_policy.rb b/app/policies/nil_class_policy.rb
new file mode 100644
index 000000000..8bce8769e
--- /dev/null
+++ b/app/policies/nil_class_policy.rb
@@ -0,0 +1,21 @@
+class NilClassPolicy < ApplicationPolicy
+ def index?
+ false
+ end
+
+ def show?
+ false
+ end
+
+ def create?
+ false
+ end
+
+ def update?
+ false
+ end
+
+ def destroy?
+ false
+ end
+end
diff --git a/app/views/emails/verify.html.erb b/app/views/emails/verify.html.erb
index 5790b4f57..f152a3558 100644
--- a/app/views/emails/verify.html.erb
+++ b/app/views/emails/verify.html.erb
@@ -6,12 +6,12 @@
<% if @user.is_restricted? %>
Your account is restricted because you signed up from a VPN or proxy.
- You can still use the site, but you won't be able to leave comments, edit
- tags, or upload posts until you verify your account.
+ You can still use the site, but you must verify your email address to be
+ able to leave comments, edit tags, or upload posts.
<% end %>
- Click below to send an email to <%= @email_address.address %>
- to verify your account.
+ Your email address is unverified. Click below to send an email to
+ <%= @email_address.address %> to verify your email address.
<%= edit_form_for(@user, method: :post, url: send_confirmation_user_email_path(@user)) do |f| %>
<%= f.submit "Send confirmation email" %>
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb
index 884a4c100..2a8ee6ec9 100644
--- a/app/views/users/_statistics.html.erb
+++ b/app/views/users/_statistics.html.erb
@@ -10,6 +10,7 @@
Join Date
<%= presenter.join_date %>
+
<% if policy(IpAddress).show? %>
Last IP
@@ -27,6 +28,33 @@
<% end %>
+ <% if policy(user.email_address).show? %>
+
+ Email Address
+
+ <% if user.email_address.present? %>
+ <%= user.email_address.address %>
+
+ <% if user == CurrentUser.user %>
+ (<%= link_to "edit", edit_user_email_path(user) %>)
+ <% end %>
+
+ <% if user.email_address.is_verified? %>
+
+ <% elsif user == CurrentUser.user %>
+ <%= link_to verify_user_email_path(user) do %>
+
+ <% end %>
+ <% else %>
+
+ <% end %>
+ <% else %>
+ none
+ <% end %>
+
+
+ <% end %>
+
Inviter
<% if user.inviter %>
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index 7d35e591f..5c2e2b583 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -114,7 +114,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
context "show action" do
setup do
# flesh out profile to get more test coverage of user presenter.
- @user = create(:banned_user, can_approve_posts: true, created_at: 2.weeks.ago)
+ @user = create(:user, can_approve_posts: true, created_at: 2.weeks.ago)
as(@user) do
create(:saved_search, user: @user)
create(:post, uploader: @user, tag_string: "fav:#{@user.name}")
@@ -152,6 +152,33 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, xml["user"]["enable_safe_mode"])
end
+ context "for a user with an email address" do
+ setup do
+ create(:email_address, user: @user)
+ end
+
+ should "show the email address to the user themselves" do
+ get_auth user_path(@user), @user
+
+ assert_response :success
+ assert_select ".user-email-address", count: 1
+ end
+
+ should "show the email address to mods" do
+ get_auth user_path(@user), create(:moderator_user)
+
+ assert_response :success
+ assert_select ".user-email-address", count: 1
+ end
+
+ should "not show the email address to other users" do
+ get_auth user_path(@user), create(:user)
+
+ assert_response :success
+ assert_select ".user-email-address", count: 0
+ end
+ end
+
context "for a tooltip" do
setup do
@banned = create(:banned_user)
From 2e633f84f6774132a440b034bb3fbb82f6a11e66 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 21:02:12 -0600
Subject: [PATCH 022/132] emails: add /emails index page.
Add emails index page at https://danbooru.donmai.us/emails. Mods can use
this page to view and search emails belonging to users below mod level.
---
app/controllers/emails_controller.rb | 13 ++++++++-
app/models/email_address.rb | 17 ++++++++++++
app/policies/email_address_policy.rb | 4 +++
app/views/emails/index.html.erb | 32 +++++++++++++++++++++++
app/views/static/site_map.html.erb | 4 +++
config/routes.rb | 1 +
test/functional/emails_controller_test.rb | 20 ++++++++++++++
7 files changed, 90 insertions(+), 1 deletion(-)
create mode 100644 app/views/emails/index.html.erb
diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb
index 37b385e48..2eb034ecf 100644
--- a/app/controllers/emails_controller.rb
+++ b/app/controllers/emails_controller.rb
@@ -1,8 +1,19 @@
class EmailsController < ApplicationController
respond_to :html, :xml, :json
+ def index
+ @email_addresses = authorize EmailAddress.visible(CurrentUser.user).paginated_search(params, count_pages: true)
+ @email_addresses = @email_addresses.includes(:user)
+ respond_with(@email_addresses)
+ end
+
def show
- @email_address = authorize EmailAddress.find_by_user_id!(params[:user_id])
+ if params[:user_id]
+ @email_address = authorize EmailAddress.find_by_user_id!(params[:user_id])
+ else
+ @email_address = authorize EmailAddress.find(params[:id])
+ end
+
respond_with(@email_address)
end
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index 367af7a97..9d4dd6f22 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -10,6 +10,14 @@ class EmailAddress < ApplicationRecord
validate :validate_deliverable, on: :deliverable
after_save :update_user
+ def self.visible(user)
+ if user.is_moderator?
+ where(user: User.where("level < ?", user.level)).or(where(user: user))
+ else
+ none
+ end
+ end
+
def address=(value)
self.normalized_address = EmailValidator.normalize(value) || address
super
@@ -29,6 +37,15 @@ class EmailAddress < ApplicationRecord
user.update!(is_verified: is_verified? && nondisposable?)
end
+ def self.search(params)
+ q = super
+
+ q = q.search_attributes(params, :user, :address, :normalized_address, :is_verified, :is_deliverable)
+ q = q.apply_default_order(params)
+
+ q
+ end
+
concerning :VerificationMethods do
def verifier
@verifier ||= Danbooru::MessageVerifier.new(:email_verification_key)
diff --git a/app/policies/email_address_policy.rb b/app/policies/email_address_policy.rb
index 152469125..f9ddcdfdb 100644
--- a/app/policies/email_address_policy.rb
+++ b/app/policies/email_address_policy.rb
@@ -1,4 +1,8 @@
class EmailAddressPolicy < ApplicationPolicy
+ def index?
+ user.is_moderator?
+ end
+
def show?
record.user_id == user.id || (user.is_moderator? && record.user.level < user.level)
end
diff --git a/app/views/emails/index.html.erb b/app/views/emails/index.html.erb
new file mode 100644
index 000000000..c1f7429ac
--- /dev/null
+++ b/app/views/emails/index.html.erb
@@ -0,0 +1,32 @@
+
+
+ <%= search_form_for(emails_path) do |f| %>
+ <%= f.simple_fields_for :user do |fa| %>
+ <%= fa.input :name, label: "User Name", input_html: { value: params.dig(:search, :user, :name), "data-autocomplete": "user" } %>
+ <% end %>
+
+ <%= f.input :address_ilike, label: "Address", input_html: { value: params[:search][:address] }, hint: "Use * for wildcard" %>
+ <%= f.input :is_verified, label: "Verified?", collection: %w[Yes No], selected: params[:search][:is_verified] %>
+ <%= f.submit "Search" %>
+ <% end %>
+
+ <%= table_for @email_addresses, class: "striped autofit" do |t| %>
+ <% t.column :user do |email| %>
+ <%= link_to_user email.user %>
+ <% end %>
+ <% t.column :address %>
+ <% t.column :is_verified, name: "Verified?" do |email| %>
+ <% if email.is_verified? %>
+ <%= link_to "Yes", emails_path(search: { is_verified: true }) %>
+ <% else %>
+ <%= link_to "No", emails_path(search: { is_verified: false }) %>
+ <% end %>
+ <% end %>
+ <% t.column :updated_at, name: "Updated" do |email| %>
+ <%= time_ago_in_words_tagged(email.updated_at) %>
+ <% end %>
+ <% end %>
+
+ <%= numbered_paginator(@email_addresses) %>
+
+
diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index b88b8c138..4e82675d2 100644
--- a/app/views/static/site_map.html.erb
+++ b/app/views/static/site_map.html.erb
@@ -154,6 +154,10 @@
<%= link_to("Moderation Reports", moderation_reports_path) %>
<% end %>
+ <% if policy(EmailAddress).index? %>
+ <%= link_to("Email Addresses", emails_path) %>
+ <% end %>
+
<% if policy(IpAddress).index? %>
<%= link_to("IP Addresses", ip_addresses_path) %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index ce3a273c1..11d7e7d28 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,7 @@ Rails.application.routes.draw do
end
resource :dtext_preview, :only => [:create]
resources :dtext_links, only: [:index]
+ resources :emails, only: [:index, :show]
resources :favorites, :only => [:index, :create, :destroy]
resources :favorite_groups do
member do
diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb
index ec08a2c03..4da9bb9a5 100644
--- a/test/functional/emails_controller_test.rb
+++ b/test/functional/emails_controller_test.rb
@@ -10,6 +10,26 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
@restricted_user = create(:user, requires_verification: true, is_verified: false)
end
+ context "#index" do
+ should "not let regular users see emails belonging to other users" do
+ get_auth emails_path, @user
+ assert_response 403
+ end
+
+ should "let mods see emails belonging to themselves and all users below mod level" do
+ @mod1 = create(:moderator_user, email_address: build(:email_address))
+ @mod2 = create(:moderator_user, email_address: build(:email_address))
+
+ get_auth emails_path, @mod1
+
+ assert_response :success
+ assert_select "#email-address-#{@user.email_address.id}", count: 1
+ assert_select "#email-address-#{@other_user.email_address.id}", count: 1
+ assert_select "#email-address-#{@mod1.email_address.id}", count: 1
+ assert_select "#email-address-#{@mod2.email_address.id}", count: 0
+ end
+ end
+
context "#show" do
should "render" do
get_auth user_email_path(@user), @user, as: :json
From eae3c1942d9f3fb011689d24e3aea3c2450942e6 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 13 Dec 2020 21:35:18 -0600
Subject: [PATCH 023/132] dmails: allow site owner to read all mails.
Allow site owner to read dmails sent to other users. This is make it
easier to investigate spam without having to drop into the dev console.
---
app/policies/dmail_policy.rb | 1 +
test/functional/dmails_controller_test.rb | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/app/policies/dmail_policy.rb b/app/policies/dmail_policy.rb
index 5d97e9916..152d0c549 100644
--- a/app/policies/dmail_policy.rb
+++ b/app/policies/dmail_policy.rb
@@ -16,6 +16,7 @@ class DmailPolicy < ApplicationPolicy
end
def show?
+ return true if user.is_owner?
user.is_member? && (record.owner_id == user.id || record.valid_key?(request.params[:key]))
end
diff --git a/test/functional/dmails_controller_test.rb b/test/functional/dmails_controller_test.rb
index 963868a64..bc2628920 100644
--- a/test/functional/dmails_controller_test.rb
+++ b/test/functional/dmails_controller_test.rb
@@ -99,6 +99,11 @@ class DmailsControllerTest < ActionDispatch::IntegrationTest
assert_response 403
end
+ should "show dmails to the site owner" do
+ get_auth dmail_path(@dmail), create(:owner_user)
+ assert_response :success
+ end
+
should "mark dmails as read" do
assert_equal(false, @dmail.is_read)
get_auth dmail_path(@dmail), @dmail.owner
From 23ee39010af61ec390d98076d4259da5ce3f90c0 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 03:00:43 -0600
Subject: [PATCH 024/132] Update ruby gems and yarn packages.
---
Gemfile.lock | 24 ++--
yarn.lock | 376 +++++++++++++++++++++++----------------------------
2 files changed, 184 insertions(+), 216 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 1bab4040e..2fb7f8a00 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -84,8 +84,8 @@ GEM
ansi (1.5.0)
ast (2.4.1)
aws-eventstream (1.1.0)
- aws-partitions (1.402.0)
- aws-sdk-core (3.109.3)
+ aws-partitions (1.405.0)
+ aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@@ -135,10 +135,10 @@ GEM
crass (1.0.6)
daemons (1.3.1)
dante (0.2.0)
- delayed_job (4.1.8)
- activesupport (>= 3.0, < 6.1)
- delayed_job_active_record (4.1.4)
- activerecord (>= 3.0, < 6.1)
+ delayed_job (4.1.9)
+ activesupport (>= 3.0, < 6.2)
+ delayed_job_active_record (4.1.5)
+ activerecord (>= 3.0, < 6.2)
delayed_job (>= 3.0, < 5)
diff-lcs (1.4.4)
docile (1.3.2)
@@ -242,7 +242,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
- puma (5.1.0)
+ puma (5.1.1)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
@@ -298,21 +298,21 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
- rubocop (1.4.2)
+ rubocop (1.6.1)
parallel (~> 1.10)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 1.8)
+ regexp_parser (>= 1.8, < 3.0)
rexml
- rubocop-ast (>= 1.1.1)
+ rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (1.3.0)
parser (>= 2.7.1.5)
- rubocop-rails (2.8.1)
+ rubocop-rails (2.9.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 0.87.0)
+ rubocop (>= 0.90.0, < 2.0)
ruby-progressbar (1.10.1)
ruby-vips (2.0.17)
ffi (~> 1.9)
diff --git a/yarn.lock b/yarn.lock
index f52a73199..bba9711e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15,42 +15,41 @@
integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==
"@babel/core@>=7.9.0", "@babel/core@^7.11.1":
- version "7.12.9"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8"
- integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd"
+ integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==
dependencies:
"@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.12.5"
+ "@babel/generator" "^7.12.10"
"@babel/helper-module-transforms" "^7.12.1"
"@babel/helpers" "^7.12.5"
- "@babel/parser" "^7.12.7"
+ "@babel/parser" "^7.12.10"
"@babel/template" "^7.12.7"
- "@babel/traverse" "^7.12.9"
- "@babel/types" "^7.12.7"
+ "@babel/traverse" "^7.12.10"
+ "@babel/types" "^7.12.10"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.1"
json5 "^2.1.2"
lodash "^4.17.19"
- resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.5.0"
-"@babel/generator@^7.12.5":
- version "7.12.5"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
- integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
+"@babel/generator@^7.12.10":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.10.tgz#2b188fc329fb8e4f762181703beffc0fe6df3460"
+ integrity sha512-6mCdfhWgmqLdtTkhXjnIz0LcdVCd26wS2JXRtj2XY0u5klDsXBREA/pG5NVOuVnF2LUrBGNFtQkIqqTbblg0ww==
dependencies:
- "@babel/types" "^7.12.5"
+ "@babel/types" "^7.12.10"
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/helper-annotate-as-pure@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
- integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==
+"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
+ integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==
dependencies:
- "@babel/types" "^7.10.4"
+ "@babel/types" "^7.12.10"
"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4":
version "7.10.4"
@@ -60,14 +59,14 @@
"@babel/helper-explode-assignable-expression" "^7.10.4"
"@babel/types" "^7.10.4"
-"@babel/helper-builder-react-jsx-experimental@^7.12.4":
- version "7.12.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz#55fc1ead5242caa0ca2875dcb8eed6d311e50f48"
- integrity sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==
+"@babel/helper-builder-react-jsx-experimental@^7.12.10":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.10.tgz#a58cb96a793dc0fcd5c9ed3bb36d62fdc60534c2"
+ integrity sha512-3Kcr2LGpL7CTRDTTYm1bzeor9qZbxbvU2AxsLA6mUG9gYarSfIKMK0UlU+azLWI+s0+BH768bwyaziWB2NOJlQ==
dependencies:
- "@babel/helper-annotate-as-pure" "^7.10.4"
- "@babel/helper-module-imports" "^7.12.1"
- "@babel/types" "^7.12.1"
+ "@babel/helper-annotate-as-pure" "^7.12.10"
+ "@babel/helper-module-imports" "^7.12.5"
+ "@babel/types" "^7.12.10"
"@babel/helper-builder-react-jsx@^7.10.4":
version "7.10.4"
@@ -132,11 +131,11 @@
"@babel/types" "^7.10.4"
"@babel/helper-get-function-arity@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
- integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
+ integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
dependencies:
- "@babel/types" "^7.10.4"
+ "@babel/types" "^7.12.10"
"@babel/helper-hoist-variables@^7.10.4":
version "7.10.4"
@@ -175,11 +174,11 @@
lodash "^4.17.19"
"@babel/helper-optimise-call-expression@^7.10.4":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz#7f94ae5e08721a49467346aa04fd22f750033b9c"
- integrity sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d"
+ integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==
dependencies:
- "@babel/types" "^7.12.7"
+ "@babel/types" "^7.12.10"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.10.4"
@@ -264,10 +263,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056"
- integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==
+"@babel/parser@^7.12.10", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.10.tgz#824600d59e96aea26a5a2af5a9d812af05c3ae81"
+ integrity sha512-PJdRPwyoOqFAWfLytxrWwGrAxghCgh/yTNCYciOz8QgjflA7aZhECPZAa2VUedKg2+QMWkI0L9lynh2SNmNEgA==
"@babel/plugin-proposal-async-generator-functions@^7.12.1":
version "7.12.1"
@@ -668,12 +667,12 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-react-jsx@^7.10.4":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz#8b14d45f6eccd41b7f924bcb65c021e9f0a06f7f"
- integrity sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.10.tgz#a7af3097c73479123594c8c8fe39545abebd44e3"
+ integrity sha512-MM7/BC8QdHXM7Qc1wdnuk73R4gbuOpfrSUgfV/nODGc86sPY1tgmY2M9E9uAnf2e4DOIp8aKGWqgZfQxnTNGuw==
dependencies:
"@babel/helper-builder-react-jsx" "^7.10.4"
- "@babel/helper-builder-react-jsx-experimental" "^7.12.4"
+ "@babel/helper-builder-react-jsx-experimental" "^7.12.10"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-jsx" "^7.12.1"
@@ -692,13 +691,12 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-runtime@^7.11.0":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz#04b792057eb460389ff6a4198e377614ea1e7ba5"
- integrity sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.10.tgz#af0fded4e846c4b37078e8e5d06deac6cd848562"
+ integrity sha512-xOrUfzPxw7+WDm9igMgQCbO3cJKymX7dFdsgRr1eu9n3KjjyU4pptIXbXPseQDquw+W+RuJEJMHKHNsPNNm3CA==
dependencies:
- "@babel/helper-module-imports" "^7.12.1"
+ "@babel/helper-module-imports" "^7.12.5"
"@babel/helper-plugin-utils" "^7.10.4"
- resolve "^1.8.1"
semver "^5.5.1"
"@babel/plugin-transform-shorthand-properties@^7.12.1":
@@ -730,10 +728,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
-"@babel/plugin-transform-typeof-symbol@^7.12.1":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a"
- integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==
+"@babel/plugin-transform-typeof-symbol@^7.12.10":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b"
+ integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
@@ -753,9 +751,9 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/preset-env@^7.11.0":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.7.tgz#54ea21dbe92caf6f10cb1a0a576adc4ebf094b55"
- integrity sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.10.tgz#ca981b95f641f2610531bd71948656306905e6ab"
+ integrity sha512-Gz9hnBT/tGeTE2DBNDkD7BiWRELZt+8lSysHuDwmYXUIvtwZl0zI+D6mZgXZX0u8YBlLS4tmai9ONNY9tjRgRA==
dependencies:
"@babel/compat-data" "^7.12.7"
"@babel/helper-compilation-targets" "^7.12.5"
@@ -816,12 +814,12 @@
"@babel/plugin-transform-spread" "^7.12.1"
"@babel/plugin-transform-sticky-regex" "^7.12.7"
"@babel/plugin-transform-template-literals" "^7.12.1"
- "@babel/plugin-transform-typeof-symbol" "^7.12.1"
+ "@babel/plugin-transform-typeof-symbol" "^7.12.10"
"@babel/plugin-transform-unicode-escapes" "^7.12.1"
"@babel/plugin-transform-unicode-regex" "^7.12.1"
"@babel/preset-modules" "^0.1.3"
- "@babel/types" "^7.12.7"
- core-js-compat "^3.7.0"
+ "@babel/types" "^7.12.10"
+ core-js-compat "^3.8.0"
semver "^5.5.0"
"@babel/preset-modules@^0.1.3":
@@ -859,25 +857,25 @@
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
-"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9", "@babel/traverse@^7.7.0":
- version "7.12.9"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f"
- integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.10.tgz#2d1f4041e8bf42ea099e5b2dc48d6a594c00017a"
+ integrity sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==
dependencies:
"@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.12.5"
+ "@babel/generator" "^7.12.10"
"@babel/helper-function-name" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.11.0"
- "@babel/parser" "^7.12.7"
- "@babel/types" "^7.12.7"
+ "@babel/parser" "^7.12.10"
+ "@babel/types" "^7.12.10"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.19"
-"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13"
- integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==
+"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
+ version "7.12.10"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.10.tgz#7965e4a7260b26f09c56bcfcb0498af1f6d9b260"
+ integrity sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
@@ -888,10 +886,10 @@
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
-"@eslint/eslintrc@^0.2.1":
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c"
- integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==
+"@eslint/eslintrc@^0.2.2":
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
+ integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
@@ -943,9 +941,9 @@
integrity sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==
"@rails/ujs@^6.0.2-1":
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.0.3.tgz#e68a03278e30daea6a110aac5dfa33c60c53055d"
- integrity sha512-CM9OEvoN9eXkaX7PXEnbsQLULJ97b9rVmwliZbz/iBOERLJ68Rk3ClJe+fQEMKU4CBZfky2lIRnfslOdUs9SLQ==
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.0.tgz#9a48df6511cb2b472c9f596c1f37dc0af022e751"
+ integrity sha512-kQNKyM4ePAc4u9eR1c4OqrbAHH+3SJXt++8izIjeaZeg+P7yBtgoF/dogMD/JPPowNC74ACFpM/4op0Ggp/fPw==
"@rails/webpacker@^5.0.0":
version "5.2.1"
@@ -1071,9 +1069,9 @@
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
"@types/node@*":
- version "14.14.10"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.10.tgz#5958a82e41863cfc71f2307b3748e3491ba03785"
- integrity sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==
+ version "14.14.13"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.13.tgz#9e425079799322113ae8477297ae6ef51b8e0cdf"
+ integrity sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -1270,7 +1268,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
-acorn-jsx@^5.2.0:
+acorn-jsx@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
@@ -1835,14 +1833,14 @@ browserify-zlib@^0.2.0:
dependencies:
pako "~1.0.5"
-browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.14.7, browserslist@^4.6.4:
- version "4.15.0"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.15.0.tgz#3d48bbca6a3f378e86102ffd017d9a03f122bdb0"
- integrity sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==
+browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0, browserslist@^4.6.4:
+ version "4.16.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b"
+ integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==
dependencies:
- caniuse-lite "^1.0.30001164"
+ caniuse-lite "^1.0.30001165"
colorette "^1.2.1"
- electron-to-chromium "^1.3.612"
+ electron-to-chromium "^1.3.621"
escalade "^3.1.1"
node-releases "^1.1.67"
@@ -1976,9 +1974,9 @@ cache-base@^1.0.1:
unset-value "^1.0.0"
cacheable-lookup@^5.0.3:
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
- integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
+ integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
cacheable-request@^7.0.1:
version "7.0.1"
@@ -2062,10 +2060,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001164:
- version "1.0.30001164"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001164.tgz#5bbfd64ca605d43132f13cc7fdabb17c3036bfdc"
- integrity sha512-G+A/tkf4bu0dSp9+duNiXc7bGds35DioCyC6vgK2m/rjA4Krpy5WeZgZyfH2f0wj2kI6yAWWucyap6oOwmY1mg==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
+ version "1.0.30001166"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001166.tgz#ca73e8747acfd16a4fd6c4b784f1b995f9698cf8"
+ integrity sha512-nCL4LzYK7F4mL0TjEMeYavafOGnBa98vTudH5c8lW9izUjnB99InG6pmC1ElAI1p0GlyZajv4ltUdFXvOHIl1A==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.3.0"
@@ -2444,23 +2442,23 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-core-js-compat@^3.7.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.0.tgz#3248c6826f4006793bd637db608bca6e4cd688b1"
- integrity sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==
+core-js-compat@^3.8.0:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e"
+ integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==
dependencies:
- browserslist "^4.14.7"
+ browserslist "^4.15.0"
semver "7.0.0"
core-js-pure@^3.0.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.0.tgz#4cdd2eca37d49cda206b66e26204818dba77884a"
- integrity sha512-fRjhg3NeouotRoIV0L1FdchA6CK7ZD+lyINyMoz19SyV+ROpC4noS1xItWHFtwZdlqfMfVPJEyEGdfri2bD1pA==
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119"
+ integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==
core-js@^3.6.5:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.0.tgz#0fc2d4941cadf80538b030648bb64d230b4da0ce"
- integrity sha512-W2VYNB0nwQQE7tKS7HzXd7r2y/y2SVJl4ga6oH/dnaLFzM0o2lB2P3zCkWj5Wc/zyMYjtgd5Hmhk0ObkQFZOIA==
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.1.tgz#f51523668ac8a294d1285c3b9db44025fda66d47"
+ integrity sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -3111,10 +3109,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-electron-to-chromium@^1.3.612:
- version "1.3.615"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz#50f523be4a04449410e9f3a694490814e602cd54"
- integrity sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==
+electron-to-chromium@^1.3.621:
+ version "1.3.625"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.625.tgz#a7bd18da4dc732c180b2e95e0e296c0bf22f3bd6"
+ integrity sha512-CsLk/r0C9dAzVPa9QF74HIXduxaucsaRfqiOYvIv2PRhvyC6EOqc/KbpgToQuDVgPf3sNAFZi3iBu4vpGOwGag==
elliptic@^6.5.3:
version "6.5.3"
@@ -3324,12 +3322,12 @@ eslint-visitor-keys@^2.0.0:
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
eslint@^7.0.0:
- version "7.14.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.14.0.tgz#2d2cac1d28174c510a97b377f122a5507958e344"
- integrity sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==
+ version "7.15.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.15.0.tgz#eb155fb8ed0865fcf5d903f76be2e5b6cd7e0bc7"
+ integrity sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==
dependencies:
"@babel/code-frame" "^7.0.0"
- "@eslint/eslintrc" "^0.2.1"
+ "@eslint/eslintrc" "^0.2.2"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@@ -3339,10 +3337,10 @@ eslint@^7.0.0:
eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^2.0.0"
- espree "^7.3.0"
+ espree "^7.3.1"
esquery "^1.2.0"
esutils "^2.0.2"
- file-entry-cache "^5.0.1"
+ file-entry-cache "^6.0.0"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^12.1.0"
@@ -3366,13 +3364,13 @@ eslint@^7.0.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
-espree@^7.3.0:
- version "7.3.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348"
- integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==
+espree@^7.3.0, espree@^7.3.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
+ integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
dependencies:
acorn "^7.4.0"
- acorn-jsx "^5.2.0"
+ acorn-jsx "^5.3.1"
eslint-visitor-keys "^1.3.0"
esprima@^4.0.0:
@@ -3639,13 +3637,6 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
-file-entry-cache@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
- integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
- dependencies:
- flat-cache "^2.0.1"
-
file-entry-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a"
@@ -3762,15 +3753,6 @@ findup-sync@^3.0.0:
micromatch "^3.0.4"
resolve-dir "^1.0.1"
-flat-cache@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
- integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
- dependencies:
- flatted "^2.0.0"
- rimraf "2.6.3"
- write "1.0.3"
-
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -3779,11 +3761,6 @@ flat-cache@^3.0.4:
flatted "^3.1.0"
rimraf "^3.0.2"
-flatted@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
- integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
-
flatted@^3.0.4, flatted@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
@@ -3803,9 +3780,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6"
follow-redirects@^1.0.0:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
- integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
+ integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
for-in@^1.0.2:
version "1.0.2"
@@ -4329,9 +4306,9 @@ html-comment-regex@^1.1.0:
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
html-entities@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44"
- integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.3.tgz#3dca638a43ee7de316fc23067398491152ad4736"
+ integrity sha512-/VulV3SYni1taM7a4RMdceqzJWR39gpZHjBwUnsCFKWV/GJkD14CJ5F7eWcZozmHJK0/f/H5U3b3SiPkuvxMgg==
html-tags@^3.1.0:
version "3.1.0"
@@ -4576,9 +4553,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
- integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-ip@^4.3.0:
version "4.3.0"
@@ -4646,9 +4623,11 @@ is-alphanumerical@^1.0.0:
is-decimal "^1.0.0"
is-arguments@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
- integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9"
+ integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==
+ dependencies:
+ call-bind "^1.0.0"
is-arrayish@^0.2.1:
version "0.2.1"
@@ -4819,9 +4798,9 @@ is-natural-number@^4.0.1:
integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
is-negative-zero@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
- integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+ integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-number@^3.0.0:
version "3.0.0"
@@ -4998,7 +4977,7 @@ js-tokens@^4.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-yaml@3.14.0, js-yaml@^3.13.1, js-yaml@^3.14.0:
+js-yaml@3.14.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
@@ -5006,6 +4985,14 @@ js-yaml@3.14.0, js-yaml@^3.13.1, js-yaml@^3.14.0:
argparse "^1.0.7"
esprima "^4.0.0"
+js-yaml@^3.13.1, js-yaml@^3.14.0:
+ version "3.14.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+ integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -5364,19 +5351,20 @@ md5.js@^1.3.4:
safe-buffer "^5.1.2"
mdast-util-from-markdown@^0.8.0:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.1.tgz#781371d493cac11212947226190270c15dc97116"
- integrity sha512-qJXNcFcuCSPqUF0Tb0uYcFDIq67qwB3sxo9RPdf9vG8T90ViKnksFqdB/Coq2a7sTnxL/Ify2y7aIQXDkQFH0w==
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.4.tgz#2882100c1b9fc967d3f83806802f303666682d32"
+ integrity sha512-jj891B5pV2r63n2kBTFh8cRI2uR9LQHsXG1zSDqfhXkIlDzrTcIlbB5+5aaYEkl8vOPIOPLf8VT7Ere1wWTMdw==
dependencies:
"@types/mdast" "^3.0.0"
- mdast-util-to-string "^1.0.0"
- micromark "~2.10.0"
+ mdast-util-to-string "^2.0.0"
+ micromark "~2.11.0"
parse-entities "^2.0.0"
+ unist-util-stringify-position "^2.0.0"
-mdast-util-to-markdown@^0.5.0:
- version "0.5.4"
- resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.5.4.tgz#be680ed0c0e11a07d07c7adff9551eec09c1b0f9"
- integrity sha512-0jQTkbWYx0HdEA/h++7faebJWr5JyBoBeiRf0u3F4F3QtnyyGaWIsOwo749kRb1ttKrLLr+wRtOkfou9yB0p6A==
+mdast-util-to-markdown@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.1.tgz#0e07d3f871e056bffc38a0cf50c7298b56d9e0d6"
+ integrity sha512-4qJtZ0qdyYeexAXoOZiU0uHIFVncJAmCkHkSluAsvDaVWODtPyNEo9I1ns0T4ulxu2EHRH5u/bt1cV0pdHCX+A==
dependencies:
"@types/unist" "^2.0.0"
longest-streak "^2.0.0"
@@ -5385,11 +5373,6 @@ mdast-util-to-markdown@^0.5.0:
repeat-string "^1.0.0"
zwitch "^1.0.0"
-mdast-util-to-string@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
- integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
-
mdast-util-to-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
@@ -5479,10 +5462,10 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-micromark@~2.10.0:
- version "2.10.1"
- resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.10.1.tgz#cd73f54e0656f10e633073db26b663a221a442a7"
- integrity sha512-fUuVF8sC1X7wsCS29SYQ2ZfIZYbTymp0EYr6sab3idFjigFFjGa5UwoniPlV9tAgntjuapW1t9U+S0yDYeGKHQ==
+micromark@~2.11.0:
+ version "2.11.2"
+ resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.2.tgz#e8b6a05f54697d2d3d27fc89600c6bc40dd05f35"
+ integrity sha512-IXuP76p2uj8uMg4FQc1cRE7lPCLsfAXuEfdjtdO55VRiFO1asrCSQ5g43NmPqFtRwzEnEhafRVzn2jg0UiKArQ==
dependencies:
debug "^4.0.0"
parse-entities "^2.0.0"
@@ -5721,11 +5704,16 @@ ms@2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-ms@2.1.2, ms@^2.1.1:
+ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+ms@^2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
multicast-dns-service-types@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
@@ -7572,11 +7560,11 @@ remark-parse@^9.0.0:
mdast-util-from-markdown "^0.8.0"
remark-stringify@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.0.tgz#8ba0c9e4167c42733832215a81550489759e3793"
- integrity sha512-8x29DpTbVzEc6Dwb90qhxCtbZ6hmj3BxWWDpMhA+1WM4dOEGH5U5/GFe3Be5Hns5MvPSFAr1e2KSVtKZkK5nUw==
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.1.tgz#576d06e910548b0a7191a71f27b33f1218862894"
+ integrity sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==
dependencies:
- mdast-util-to-markdown "^0.5.0"
+ mdast-util-to-markdown "^0.6.0"
remark@^13.0.0:
version "13.0.0"
@@ -7609,11 +7597,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
-replace-ext@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
- integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
-
request@^2.87.0, request@^2.88.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
@@ -7700,7 +7683,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1:
+resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.17.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
@@ -7747,13 +7730,6 @@ rimraf@2, rimraf@^2.5.4, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
-rimraf@2.6.3:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
- integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
- dependencies:
- glob "^7.1.3"
-
rimraf@3.0.2, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -9249,13 +9225,12 @@ vfile-message@^2.0.0:
unist-util-stringify-position "^2.0.0"
vfile@^4.0.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01"
- integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624"
+ integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==
dependencies:
"@types/unist" "^2.0.0"
is-buffer "^2.0.0"
- replace-ext "1.0.0"
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
@@ -9506,13 +9481,6 @@ write-file-atomic@^3.0.3:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
-write@1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
- integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
- dependencies:
- mkdirp "^0.5.1"
-
ws@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
@@ -9521,9 +9489,9 @@ ws@^6.2.1:
async-limiter "~1.0.0"
xregexp@^4.2.4:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.4.0.tgz#29660f5d6567cd2ef981dd4a50cb05d22c10719d"
- integrity sha512-83y4aa8o8o4NZe+L+46wpa+F1cWR/wCGOWI3tzqUso0w3/KAvXy0+Di7Oe/cbNMixDR4Jmi7NEybWU6ps25Wkg==
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.4.1.tgz#c84a88fa79e9ab18ca543959712094492185fe65"
+ integrity sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==
dependencies:
"@babel/runtime-corejs3" "^7.12.1"
From 852c67f1b792984612ba898943817012c219548c Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 14:13:11 -0600
Subject: [PATCH 025/132] users: fix #owner method.
---
app/models/user.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/user.rb b/app/models/user.rb
index ec14ed950..7ac2d8aea 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -193,7 +193,7 @@ class User < ApplicationRecord
module ClassMethods
def owner
- User.find_by!(level: Levels::ADMIN)
+ User.find_by!(level: Levels::OWNER)
end
def system
From 0150911343699c8215dc0d00a1e690cabeae4123 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 14:29:31 -0600
Subject: [PATCH 026/132] css: remove missing --dtext-expand-border-color var.
---
app/javascript/src/styles/common/dtext.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/javascript/src/styles/common/dtext.scss b/app/javascript/src/styles/common/dtext.scss
index 40627770d..5c628c9ba 100644
--- a/app/javascript/src/styles/common/dtext.scss
+++ b/app/javascript/src/styles/common/dtext.scss
@@ -97,7 +97,6 @@ div.prose {
div.expandable-content {
display: none;
padding: 0.4em;
- border-top: 1px solid var(--dtext-expand-border-color);
> :last-child {
margin-bottom: 0;
From df1404b6731b64b0726adca1e7c47a4461c14a81 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 14:48:06 -0600
Subject: [PATCH 027/132] js: set SameSite=Lax on cookies set by Javascript.
This is the new default for most browsers nowadays. Fixes a warning in
Firefox about using SameSite=None without the Secure flag.
---
app/javascript/src/javascripts/cookie.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/javascript/src/javascripts/cookie.js b/app/javascript/src/javascripts/cookie.js
index 20a5e4b0b..1c457d11b 100644
--- a/app/javascript/src/javascripts/cookie.js
+++ b/app/javascript/src/javascripts/cookie.js
@@ -14,7 +14,7 @@ Cookie.put = function(name, value, days) {
expires = "expires=" + date.toGMTString() + "; ";
}
- var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/";
+ var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/; SameSite=Lax";
if (document.cookie.length < (4090 - new_val.length)) {
document.cookie = new_val;
return true;
From 23f6b8a46d783b9c9eae8a8df41bc00a03a282fc Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 14:58:32 -0600
Subject: [PATCH 028/132] js: refactor Cookie.put.
* Set Max-Age= flag instead of Expires= flag.
* Set Secure flag when using HTTPS.
* Extend default cookie lifetime from 1 year to 20 years.
* Remove "session" expiration option (unused).
* Remove max cookie size check.
The cookie size check was previously added in #2518 to deal with running
out of space due to tag scripts and blacklists. This should no longer
happen since we no longer use cookies for these things. Remove the
warning because it should never happen, we can't fix it if it does, and
the user probably won't know how to fix it either.
---
app/javascript/src/javascripts/cookie.js | 26 ++++++++----------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/app/javascript/src/javascripts/cookie.js b/app/javascript/src/javascripts/cookie.js
index 1c457d11b..707d95389 100644
--- a/app/javascript/src/javascripts/cookie.js
+++ b/app/javascript/src/javascripts/cookie.js
@@ -1,27 +1,17 @@
-import Utility from "./utility";
-
let Cookie = {};
-Cookie.put = function(name, value, days) {
- var expires = "";
- if (days !== "session") {
- if (!days) {
- days = 365;
- }
+Cookie.put = function(name, value, max_age_in_days = 365 * 20) {
+ let cookie = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax;`;
- var date = new Date();
- date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
- expires = "expires=" + date.toGMTString() + "; ";
+ if (max_age_in_days) {
+ cookie += ` Max-Age=${max_age_in_days * 24 * 60 * 60};`
}
- var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/; SameSite=Lax";
- if (document.cookie.length < (4090 - new_val.length)) {
- document.cookie = new_val;
- return true;
- } else {
- Utility.error("You have too many cookies on this site. Consider deleting them all.")
- return false;
+ if (location.protocol === "https:") {
+ cookie += " Secure;";
}
+
+ document.cookie = cookie;
}
Cookie.raw_get = function(name) {
From c02c31b966336a5e91cc4f3d0951d12f26d0fd88 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 16:48:21 -0600
Subject: [PATCH 029/132] autocomplete: recognize Japanese tags in
autocomplete.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Allowing typing Japanese tags in autocomplete. For example, typing 東方
in autocomplete will be completed to the touhou tag. Typing ぶくぶ will
complete to the bkub tag.
This works using wiki page and artist other names. Effectively, any name
listed as an other name in a wiki or artist page will be treated like an
alias for autocomplete purposes. This is limited to non-ASCII other names,
to prevent English other names from interfering with regular tag searches.
---
app/logical/autocomplete_service.rb | 23 +++++++++++++++++++++-
app/models/wiki_page.rb | 2 +-
test/unit/autocomplete_service_test.rb | 27 ++++++++++++++++++++++++++
3 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 833c016da..2d4f5d614 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -72,8 +72,11 @@ class AutocompleteService
results = results.uniq.sort_by { |r| [r[:antecedent].length, -r[:post_count]] }.take(limit)
elsif string.include?("*")
results = tag_matches(string)
+ results = tag_other_name_matches(string) if results.blank?
else
- results = tag_matches(string + "*")
+ string += "*"
+ results = tag_matches(string)
+ results = tag_other_name_matches(string) if results.blank?
results = tag_autocorrect_matches(string) if results.blank?
end
@@ -81,6 +84,8 @@ class AutocompleteService
end
def tag_matches(string)
+ return [] if string =~ /[^[:ascii:]]/
+
name_matches = Tag.nonempty.name_matches(string).order(post_count: :desc).limit(limit)
alias_matches = Tag.nonempty.alias_matches(string).order(post_count: :desc).limit(limit)
union = "((#{name_matches.to_sql}) UNION (#{alias_matches.to_sql})) AS tags"
@@ -100,6 +105,7 @@ class AutocompleteService
end
def tag_autocorrect_matches(string)
+ string = string.delete("*")
tags = Tag.nonempty.autocorrect_matches(string).limit(limit)
tags.map do |tag|
@@ -107,6 +113,21 @@ class AutocompleteService
end
end
+ def tag_other_name_matches(string)
+ return [] unless string =~ /[^[:ascii:]]/
+
+ artists = Artist.undeleted.any_other_name_like(string)
+ wikis = WikiPage.undeleted.other_names_match(string)
+ tags = Tag.where(name: wikis.select(:title)).or(Tag.where(name: artists.select(:name)))
+ tags = tags.nonempty.order(post_count: :desc).limit(limit).includes(:wiki_page, :artist)
+
+ tags.map do |tag|
+ other_names = tag.artist&.other_names.to_a + tag.wiki_page&.other_names.to_a
+ antecedent = other_names.find { |other_name| other_name.ilike?(string) }
+ { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
+ end
+ end
+
def autocomplete_metatag(metatag, value)
results = case metatag.to_sym
when :user, :approver, :commenter, :comm, :noter, :noteupdater, :commentaryupdater,
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index c9fd1a7c1..603ddcecd 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -45,7 +45,7 @@ class WikiPage < ApplicationRecord
def other_names_match(name)
if name =~ /\*/
- subquery = WikiPage.from("unnest(other_names) AS other_name").where_ilike("other_name", name)
+ subquery = WikiPage.from("unnest(other_names) AS other_name").where_ilike("other_name", normalize_other_name(name))
where(id: subquery)
else
other_names_include(name)
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index 29d8ea6ce..eaf65235b 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -97,6 +97,33 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
end
+ should "autocomplete tags from wiki and artist other names" do
+ create(:tag, name: "touhou")
+ create(:tag, name: "bkub", category: Tag.categories.artist)
+ create(:wiki_page, title: "touhou", other_names: %w[東方 东方 동방])
+ create(:artist, name: "bkub", other_names: %w[大川ぶくぶ フミンバイン])
+
+ assert_autocomplete_equals(["touhou"], "東", :tag_query)
+ assert_autocomplete_equals(["touhou"], "东", :tag_query)
+ assert_autocomplete_equals(["touhou"], "동", :tag_query)
+
+ assert_autocomplete_equals(["touhou"], "*東*", :tag_query)
+ assert_autocomplete_equals(["touhou"], "東*", :tag_query)
+ assert_autocomplete_equals([], "*東", :tag_query)
+
+ assert_autocomplete_equals(["touhou"], "*方*", :tag_query)
+ assert_autocomplete_equals(["touhou"], "*方", :tag_query)
+ assert_autocomplete_equals([], "方", :tag_query)
+
+ assert_autocomplete_equals(["bkub"], "*大*", :tag_query)
+ assert_autocomplete_equals(["bkub"], "大", :tag_query)
+ assert_autocomplete_equals([], "*大", :tag_query)
+
+ assert_autocomplete_equals(["bkub"], "*川*", :tag_query)
+ assert_autocomplete_equals([], "*川", :tag_query)
+ assert_autocomplete_equals([], "川", :tag_query)
+ end
+
should "autocomplete wildcard searches" do
create(:tag, name: "mole", post_count: 150)
create(:tag, name: "mole_under_eye", post_count: 100)
From 4cdaf7bcdfdb983b1f0648af2b8eb527e8118b7f Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 17:40:41 -0600
Subject: [PATCH 030/132] autocomplete: update html data attributes.
* Remove the `source` and `weight` html data attributes (no longer used).
* Make the `type` html data attribute properly indicate the completion
type. Valid types: `tag`, `tag-alias`, `tag-abbreviation`,
`tag-autocorrect`, `tag-other-name`.
---
app/javascript/src/javascripts/autocomplete.js.erb | 6 ++----
app/javascript/src/styles/common/autocomplete.scss | 2 +-
app/logical/autocomplete_service.rb | 10 ++++++----
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index 2d4818a95..de025cd66 100644
--- a/app/javascript/src/javascripts/autocomplete.js.erb
+++ b/app/javascript/src/javascripts/autocomplete.js.erb
@@ -180,10 +180,8 @@ Autocomplete.render_item = function(list, item) {
$link.append($post_count);
}
- if (item.type === "tag") {
+ if (/^tag/.test(item.type)) {
$link.addClass("tag-type-" + item.category);
- } else if (item.type === "tag_autocorrect") {
- $link.addClass(`tag-type-${item.category} tag-type-autocorrect`);
} else if (item.type === "user") {
var level_class = "user-" + item.level.toLowerCase();
$link.addClass(level_class);
@@ -194,7 +192,7 @@ Autocomplete.render_item = function(list, item) {
var $menu_item = $("
").append($link);
var $list_item = $(" ").data("item.autocomplete", item).append($menu_item);
- var data_attributes = ["type", "source", "antecedent", "value", "category", "post_count", "weight"];
+ var data_attributes = ["type", "antecedent", "value", "category", "post_count"];
data_attributes.forEach(attr => {
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
});
diff --git a/app/javascript/src/styles/common/autocomplete.scss b/app/javascript/src/styles/common/autocomplete.scss
index 44158a614..e8a30e453 100644
--- a/app/javascript/src/styles/common/autocomplete.scss
+++ b/app/javascript/src/styles/common/autocomplete.scss
@@ -22,7 +22,7 @@
color: var(--autocomplete-arrow-color);
}
- a.tag-type-autocorrect .autocomplete-antecedent {
+ li[data-autocomplete-type="tag-autocorrect"] .autocomplete-antecedent {
text-decoration: dotted underline;
}
}
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 2d4f5d614..845376885 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -92,7 +92,9 @@ class AutocompleteService
tags = Tag.from(union).order(post_count: :desc).limit(limit).includes(:consequent_aliases)
tags.map do |tag|
- { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: tag.tag_alias_for_pattern(string)&.antecedent_name }
+ antecedent = tag.tag_alias_for_pattern(string)&.antecedent_name
+ type = antecedent.present? ? "tag-alias" : "tag"
+ { type: type, label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
end
end
@@ -100,7 +102,7 @@ class AutocompleteService
tags = Tag.nonempty.abbreviation_matches(string).order(post_count: :desc).limit(limit)
tags.map do |tag|
- { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: "/" + tag.abbreviation }
+ { type: "tag-abbreviation", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: "/" + tag.abbreviation }
end
end
@@ -109,7 +111,7 @@ class AutocompleteService
tags = Tag.nonempty.autocorrect_matches(string).limit(limit)
tags.map do |tag|
- { type: "tag_autocorrect", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: string }
+ { type: "tag-autocorrect", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: string }
end
end
@@ -124,7 +126,7 @@ class AutocompleteService
tags.map do |tag|
other_names = tag.artist&.other_names.to_a + tag.wiki_page&.other_names.to_a
antecedent = other_names.find { |other_name| other_name.ilike?(string) }
- { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
+ { type: "tag-other-name", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: antecedent }
end
end
From cf70a651f03a1dc0b8c815094a697350a1497625 Mon Sep 17 00:00:00 2001
From: Vladimir-A <32281993+Vladimir-A@users.noreply.github.com>
Date: Tue, 15 Dec 2020 05:01:25 +0300
Subject: [PATCH 031/132] Add: apt-get -y install gcc g++
gcc, g ++ is required to building
---
INSTALL.debian | 1 +
1 file changed, 1 insertion(+)
diff --git a/INSTALL.debian b/INSTALL.debian
index c435d7748..f158d1266 100644
--- a/INSTALL.debian
+++ b/INSTALL.debian
@@ -48,6 +48,7 @@ apt-get -y install zlib1g-dev libglib2.0-dev
apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel redis git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
apt-get -y install libpq-dev postgresql-client
apt-get -y install liblcms2-dev $LIBJPEG_TURBO_DEV_PKG libexpat1-dev libgif-dev libpng-dev libexif-dev
+apt-get -y install gcc g++
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
From 0f44d8b974d9f7421ba281d6ac358672731449b9 Mon Sep 17 00:00:00 2001
From: Vladimir-A <32281993+Vladimir-A@users.noreply.github.com>
Date: Tue, 15 Dec 2020 05:15:57 +0300
Subject: [PATCH 032/132] Change: regular expressions.
Add [^ -] to exclude duplicate.
Example:
PostgreSQL 11.9 (Debian 11.9-0+deb10u1)
---
INSTALL.debian | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/INSTALL.debian b/INSTALL.debian
index f158d1266..a471b1eeb 100644
--- a/INSTALL.debian
+++ b/INSTALL.debian
@@ -77,7 +77,7 @@ chsh -s /bin/bash danbooru
usermod -G danbooru,sudo danbooru
# Set up Postgres
-export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
+export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}[^-]'`
if verlte 9.5 $PG_VERSION ; then
# only do this on postgres 9.5 and above
From 26246b0ac96ac5d3d09f3a67e2f8d8bc1836576b Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 14 Dec 2020 21:57:28 -0600
Subject: [PATCH 033/132] autocomplete: fix exception when typing "/" in
autocomplete.
Fix an exception that could occur when typing "/" by itself in
autocomplete and a regular tag starting with "/" was returned. This
caused an exception in `r[:antecedent].length` because the tag's
antecedent was nil.
---
app/logical/autocomplete_service.rb | 3 ++-
test/unit/autocomplete_service_test.rb | 34 +++++++++++++++++---------
2 files changed, 25 insertions(+), 12 deletions(-)
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 845376885..0c5d39f93 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -69,7 +69,8 @@ class AutocompleteService
string = string + "*" unless string.include?("*")
results = tag_matches(string)
results += tag_abbreviation_matches(string)
- results = results.uniq.sort_by { |r| [r[:antecedent].length, -r[:post_count]] }.take(limit)
+ results = results.sort_by { |r| [r[:antecedent].to_s.size, -r[:post_count]] }
+ results = results.uniq { |r| r[:value] }.take(limit)
elsif string.include?("*")
results = tag_matches(string)
results = tag_other_name_matches(string) if results.blank?
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index eaf65235b..de2e547b3 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -82,19 +82,31 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_includes("touhou", "~tou", :tag_query)
end
- should "autocomplete tag abbreviations" do
- create(:tag, name: "mole", post_count: 150)
- create(:tag, name: "mole_under_eye", post_count: 100)
- create(:tag, name: "mole_under_mouth", post_count: 50)
+ context "for a tag abbreviation" do
+ should "autocomplete abbreviations" do
+ create(:tag, name: "mole", post_count: 150)
+ create(:tag, name: "mole_under_eye", post_count: 100)
+ create(:tag, name: "mole_under_mouth", post_count: 50)
- assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "/m", :tag_query)
- assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "/mu", :tag_query)
- assert_autocomplete_equals(%w[mole_under_mouth], "/mum", :tag_query)
- assert_autocomplete_equals(%w[mole_under_eye], "/mue", :tag_query)
- assert_autocomplete_equals(%w[mole_under_eye], "/*ue", :tag_query)
+ assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "/m", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "/mu", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_mouth], "/mum", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye], "/mue", :tag_query)
+ assert_autocomplete_equals(%w[mole_under_eye], "/*ue", :tag_query)
- assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
- assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
+ assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
+ assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
+ end
+
+ should "work for regular tags starting with a /" do
+ create(:tag, name: "jojo_pose", post_count: 100)
+ create(:tag, name: "/jp/", post_count: 50)
+
+ assert_autocomplete_equals(%w[/jp/ jojo_pose], "/", :tag_query)
+ assert_autocomplete_equals(%w[/jp/ jojo_pose], "/j", :tag_query)
+ assert_autocomplete_equals(%w[/jp/ jojo_pose], "/jp", :tag_query)
+ assert_autocomplete_equals(%w[/jp/], "/jp/", :tag_query)
+ end
end
should "autocomplete tags from wiki and artist other names" do
From c836c93b818ee6a56d3a8ece36eb380f42ba5bf3 Mon Sep 17 00:00:00 2001
From: evazion
Date: Tue, 15 Dec 2020 03:43:13 -0600
Subject: [PATCH 034/132] autocomplete: don't send cookies in publicly cached
responses.
Fix session cookies being sent in publicly cached /autocomplete.json
responses. We can't set any cookies in a response that is being publicly
cached, otherwise they'll be visible to other users. If a user's session
cookies were to be cached, then it would allow their account to be stolen.
In reality, well-behaved caches like Cloudflare will simply refuse to
cache responses that contain cookies to avoid this scenario.
https://support.cloudflare.com/hc/en-us/articles/200172516-Understanding-Cloudflare-s-CDN:
BYPASS is returned when enabling Origin Cache-Control. Cloudflare also
sets BYPASS when your origin web server sends cookies in the response
header.
---
app/controllers/application_controller.rb | 9 +++++++++
test/functional/autocomplete_controller_test.rb | 8 ++++++++
2 files changed, 17 insertions(+)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9afec6b03..8f6faeb53 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
before_action :set_variant
before_action :add_headers
before_action :cause_error
+ after_action :skip_session_if_publicly_cached
after_action :reset_current_user
layout "default"
@@ -148,6 +149,14 @@ class ApplicationController < ActionController::Base
CurrentUser.root_url = root_url.chomp("/")
end
+ # Skip setting the session cookie if the response is being publicly cached to
+ # prevent the user's session cookie from being leaked to other users.
+ def skip_session_if_publicly_cached
+ if response.cache_control[:public] == true
+ request.session_options[:skip] = true
+ end
+ end
+
def set_variant
request.variant = params[:variant].try(:to_sym)
end
diff --git a/test/functional/autocomplete_controller_test.rb b/test/functional/autocomplete_controller_test.rb
index 4dd62ee1a..ae372ab1f 100644
--- a/test/functional/autocomplete_controller_test.rb
+++ b/test/functional/autocomplete_controller_test.rb
@@ -34,6 +34,14 @@ class AutocompleteControllerTest < ActionDispatch::IntegrationTest
assert_autocomplete_equals(["rating:safe"], "rating:s", "tag_query")
assert_autocomplete_equals(["rating:safe"], "-rating:s", "tag_query")
end
+
+ should "not set session cookies when the response is publicly cached" do
+ get autocomplete_index_path(search: { query: "azur", type: "tag_query" }), as: :json
+
+ assert_response :success
+ assert_equal(true, response.cache_control[:public])
+ assert_equal({}, response.cookies)
+ end
end
end
end
From 0d83106a215d27288d3db00fcf102dba41df94cf Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 16 Dec 2020 01:31:32 -0600
Subject: [PATCH 035/132] autocomplete: fix cache issue related to content
negotiation.
This is the scenario:
* You type something in autocomplete, let's say 'touhou'.
* Autocomplete calls /autocomplete?search[query]=touhou&search[type]=tag_query
* The endpoint returns JSON, because the autocomplete call sets an
`Accept: application/json` header requesting JSON.
* Visit /autocomplete?search[query]=touhou&search[type]=tag_query in your browser.
* Notice that the cached JSON response is incorrectly returned, not an
HTML response like the browser requested.
The problem is that the response type is chosen based on the Accept
header, but the response didn't set the `Vary: Accept` header, so the
browser doesn't know the response type can vary and so it incorrectly
returns the cached response.
This issue is partially fixed by Rails 6.1 ([1]), which properly sets the
`Vary: Accept` header when the response depends on the Accept header.
However, the next issue is that Cloudflare doesn't respect the Vary
header at all ([2], [3]). Therefore we can't use the Accept header to
pick the format, instead we have explicitly specify the format with
/autocomplete.json.
This is clearer and better for caching anyway. Using the `Vary: Accept`
header reduces the cache hit rate, because the exact format of the
Accept header varies across browsers, which fragments the cache.
Whew.
[1] https://github.com/rails/rails/pull.36213
[2] https://community.cloudflare.com/t/cloudflare-cdn-cache-to-support-http-vary-header/160802
[3] https://support.cloudflare.com/hc/en-us/articles/115003206852
[4] https://www.smashingmagazine.com/2017/11/understanding-vary-header/
---
app/javascript/src/javascripts/autocomplete.js.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index de025cd66..4046ccc7d 100644
--- a/app/javascript/src/javascripts/autocomplete.js.erb
+++ b/app/javascript/src/javascripts/autocomplete.js.erb
@@ -201,7 +201,7 @@ Autocomplete.render_item = function(list, item) {
};
Autocomplete.autocomplete_source = function(query, type) {
- return $.getJSON("/autocomplete", {
+ return $.getJSON("/autocomplete.json", {
"search[query]": query,
"search[type]": type,
"limit": Autocomplete.MAX_RESULTS
From 6b966689b071eb8bb030b6751c1e22833a28cff2 Mon Sep 17 00:00:00 2001
From: nonamethanks
Date: Wed, 16 Dec 2020 13:42:25 +0100
Subject: [PATCH 036/132] Blacklist pixiv en urls from artist finder
---
app/logical/artist_finder.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/logical/artist_finder.rb b/app/logical/artist_finder.rb
index a0e8f21b9..205a3cd9a 100644
--- a/app/logical/artist_finder.rb
+++ b/app/logical/artist_finder.rb
@@ -83,8 +83,8 @@ module ArtistFinder
"pixiv.net", # https://www.pixiv.net/member.php?id=10442390
"pixiv.net/stacc", # https://www.pixiv.net/stacc/aaaninja2013
"pixiv.net/fanbox/creator", # https://www.pixiv.net/fanbox/creator/310630
- "pixiv.net/users", # https://www.pixiv.net/users/555603
- "pixiv.net/en/users", # https://www.pixiv.net/en/users/555603
+ %r{pixiv.net/(?:en/)?users}i, # https://www.pixiv.net/users/555603
+ %r{pixiv.net/(?:en/)?artworks}i, # https://www.pixiv.net/en/artworks/85241178
"i.pximg.net",
"plurk.com", # http://www.plurk.com/a1amorea1a1
"privatter.net",
From 25682ebf4669f764ee65d3d485883d71569ef820 Mon Sep 17 00:00:00 2001
From: nonamethanks
Date: Wed, 16 Dec 2020 13:43:50 +0100
Subject: [PATCH 037/132] Blacklist baraag.net root from artist finder
---
app/logical/artist_finder.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/logical/artist_finder.rb b/app/logical/artist_finder.rb
index 205a3cd9a..a9ba53edf 100644
--- a/app/logical/artist_finder.rb
+++ b/app/logical/artist_finder.rb
@@ -8,6 +8,7 @@ module ArtistFinder
"www.artstation.com", # http://www.artstation.com/serafleur/
%r{cdn[ab]?\.artstation\.com/p/assets/images/images}i, # https://cdna.artstation.com/p/assets/images/images/001/658/068/large/yang-waterkuma-b402.jpg?1450269769
"ask.fm", # http://ask.fm/mikuroko_396
+ "baraag.net",
"bcyimg.com",
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
"bcy.net",
From 3801e08ae6eee7ea389f9d50ce9d383fa62072ee Mon Sep 17 00:00:00 2001
From: nonamethanks
Date: Wed, 16 Dec 2020 13:53:16 +0100
Subject: [PATCH 038/132] Update pixiv url matching tests
---
test/unit/artist_test.rb | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/test/unit/artist_test.rb b/test/unit/artist_test.rb
index 3650b05af..a7fefeb7e 100644
--- a/test/unit/artist_test.rb
+++ b/test/unit/artist_test.rb
@@ -164,6 +164,13 @@ class ArtistTest < ActiveSupport::TestCase
assert_artist_not_found("http://i2.pixiv.net/img28/img/kyang692/35563903.jpg")
end
+ should "ignore /en/ pixiv url matches" do
+ a1 = FactoryBot.create(:artist, :name => "vvv", :url_string => "https://www.pixiv.net/en/users/32072927/artworks")
+ a2 = FactoryBot.create(:artist, :name => "c01a", :url_string => "https://www.pixiv.net/en/users/31744504")
+ assert_artist_not_found("https://www.pixiv.net/en/artworks/85241178")
+ assert_artist_not_found("https://www.pixiv.net/en/users/85241178")
+ end
+
should "find matches by url" do
a1 = FactoryBot.create(:artist, :name => "rembrandt", :url_string => "http://rembrandt.com/x/test.jpg")
a2 = FactoryBot.create(:artist, :name => "subway", :url_string => "http://subway.com/x/test.jpg")
From 2297bf5da58c3c83c9dcff8ddff21d5303e4e322 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 16 Dec 2020 20:03:09 -0600
Subject: [PATCH 039/132] Fix #4638: Add exclusions to the numeric attributes.
Add the following search operators:
* /tags?search[post_count_eq]=42
* /tags?search[post_count_not_eq]=42
* /tags?search[post_count_gt]=42
* /tags?search[post_count_gteq]=42
* /tags?search[post_count_lt]=42
* /tags?search[post_count_lteq]=42
Works for all numeric attributes on all index actions.
---
app/logical/concerns/searchable.rb | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index 3cde3b5ec..4b64261eb 100644
--- a/app/logical/concerns/searchable.rb
+++ b/app/logical/concerns/searchable.rb
@@ -183,7 +183,7 @@ module Searchable
when :boolean
search_boolean_attribute(name, params)
when :integer, :datetime
- numeric_attribute_matches(name, params[name])
+ search_numeric_attribute(name, params)
when :inet
search_inet_attribute(name, params)
when :enum
@@ -196,6 +196,26 @@ module Searchable
end
end
+ def search_numeric_attribute(attr, params)
+ if params[attr].present?
+ numeric_attribute_matches(attr, params[attr])
+ elsif params[:"#{attr}_eq"].present?
+ where_operator(attr, :eq, params[:"#{attr}_eq"])
+ elsif params[:"#{attr}_not_eq"].present?
+ where_operator(attr, :not_eq, params[:"#{attr}_not_eq"])
+ elsif params[:"#{attr}_gt"].present?
+ where_operator(attr, :gt, params[:"#{attr}_gt"])
+ elsif params[:"#{attr}_gteq"].present?
+ where_operator(attr, :gteq, params[:"#{attr}_gteq"])
+ elsif params[:"#{attr}_lt"].present?
+ where_operator(attr, :lt, params[:"#{attr}_lt"])
+ elsif params[:"#{attr}_lteq"].present?
+ where_operator(attr, :lteq, params[:"#{attr}_lteq"])
+ else
+ all
+ end
+ end
+
def search_text_attribute(attr, params)
if params[attr].present?
where(attr => params[attr])
From b0659eb76ca382593f22a0e1f0213b614a797f02 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 16 Dec 2020 21:11:16 -0600
Subject: [PATCH 040/132] searchable: add tests for Searchable concern.
---
test/unit/concerns/searchable.rb | 124 +++++++++++++++++++++++++++++++
1 file changed, 124 insertions(+)
create mode 100644 test/unit/concerns/searchable.rb
diff --git a/test/unit/concerns/searchable.rb b/test/unit/concerns/searchable.rb
new file mode 100644
index 000000000..8dace9e16
--- /dev/null
+++ b/test/unit/concerns/searchable.rb
@@ -0,0 +1,124 @@
+require 'test_helper'
+
+class SearchableTest < ActiveSupport::TestCase
+ def assert_search_equals(results, **params)
+ assert_equal(Array(results).map(&:id), subject.search(**params).ids)
+ end
+
+ context "#search method" do
+ subject { Post }
+
+ setup do
+ @p1 = create(:post, source: "a1", score: 1, is_deleted: true, uploader_ip_addr: "10.0.0.1")
+ @p2 = create(:post, source: "b2", score: 2, is_deleted: false)
+ @p3 = create(:post, source: "c3", score: 3, is_deleted: false)
+ end
+
+ context "for a numeric attribute" do
+ should "support basic operators" do
+ assert_search_equals(@p1, score_eq: 1)
+ assert_search_equals(@p3, score_gt: 2)
+ assert_search_equals(@p1, score_lt: 2)
+ assert_search_equals([@p3, @p1], score_not_eq: 2)
+ assert_search_equals([@p3, @p2], score_gteq: 2)
+ assert_search_equals([@p2, @p1], score_lteq: 2)
+ end
+
+ should "support embedded expressions" do
+ assert_search_equals(@p1, score: "1")
+ assert_search_equals(@p3, score: ">2")
+ assert_search_equals(@p1, score: "<2")
+ assert_search_equals([@p3, @p2], score: ">=2")
+ assert_search_equals([@p2, @p1], score: "<=2")
+ assert_search_equals([@p3, @p2], score: "3,2")
+ assert_search_equals([@p2, @p1], score: "1...3")
+ assert_search_equals([@p2, @p1], score: "3...1")
+ assert_search_equals([@p3, @p2, @p1], score: "1..3")
+ assert_search_equals([@p3, @p2, @p1], score: "3..1")
+ end
+ end
+
+ context "for a string attribute" do
+ should "support various operators" do
+ assert_search_equals(@p1, source: "a1")
+ assert_search_equals(@p1, source_eq: "a1")
+ assert_search_equals(@p1, source_like: "a*")
+ assert_search_equals(@p1, source_ilike: "A*")
+ assert_search_equals(@p1, source_regex: "^a.*")
+
+ assert_search_equals(@p1, source_array: ["a1", "blah"])
+ assert_search_equals(@p1, source_comma: "a1,blah")
+ assert_search_equals(@p1, source_space: "a1 blah")
+ assert_search_equals(@p1, source_lower_array: ["a1", "BLAH"])
+ assert_search_equals(@p1, source_lower_comma: "a1,BLAH")
+ assert_search_equals(@p1, source_lower_space: "a1 BLAH")
+
+ assert_search_equals([@p3, @p2], source_not_eq: "a1")
+ assert_search_equals([@p3, @p2], source_not_like: "a*")
+ assert_search_equals([@p3, @p2], source_not_ilike: "A*")
+ assert_search_equals([@p3, @p2], source_not_regex: "^a.*")
+ end
+ end
+
+ context "for a boolean attribute" do
+ should "work" do
+ assert_search_equals(@p1, is_deleted: "true")
+ assert_search_equals(@p1, is_deleted: "yes")
+ assert_search_equals(@p1, is_deleted: "on")
+ assert_search_equals(@p1, is_deleted: "1")
+
+ assert_search_equals([@p3, @p2], is_deleted: "false")
+ assert_search_equals([@p3, @p2], is_deleted: "no")
+ assert_search_equals([@p3, @p2], is_deleted: "off")
+ assert_search_equals([@p3, @p2], is_deleted: "0")
+ end
+ end
+
+ context "for an inet attribute" do
+ should "work" do
+ assert_search_equals(@p1, uploader_ip_addr: "10.0.0.1")
+ assert_search_equals(@p1, uploader_ip_addr: "10.0.0.1/24")
+ assert_search_equals(@p1, uploader_ip_addr: "10.0.0.1,1.1.1.1")
+ assert_search_equals(@p1, uploader_ip_addr: "10.0.0.1 1.1.1.1")
+ end
+ end
+
+ context "for an enum attribute" do
+ subject { PostFlag }
+
+ should "work" do
+ @pf = create(:post_flag, status: :pending)
+
+ assert_search_equals(@pf, status: "pending")
+ assert_search_equals(@pf, status: "pending,blah")
+ assert_search_equals(@pf, status: "pending blah")
+ assert_search_equals(@pf, status_id: 0)
+ end
+ end
+
+ context "for an array attribute" do
+ subject { WikiPage }
+
+ should "work" do
+ @wp = create(:wiki_page, other_names: ["a1", "b2"])
+
+ assert_search_equals(@wp, other_names_include_any: "a1")
+ assert_search_equals(@wp, other_names_include_any: "a1 blah")
+
+ assert_search_equals(@wp, other_names_include_all: "a1")
+ assert_search_equals(@wp, other_names_include_all: "a1 b2")
+
+ assert_search_equals(@wp, other_names_include_any_array: ["a1", "blah"])
+ assert_search_equals(@wp, other_names_include_all_array: ["a1", "b2"])
+
+ assert_search_equals(@wp, other_names_include_any_lower: "A1 BLAH")
+ assert_search_equals(@wp, other_names_include_all_lower: "A1 B2")
+
+ assert_search_equals(@wp, other_names_include_any_lower_array: ["A1", "BLAH"])
+ assert_search_equals(@wp, other_names_include_all_lower_array: ["A1", "B2"])
+
+ assert_search_equals(@wp, other_name_count: 2)
+ end
+ end
+ end
+end
From e771c0fca820dadafe40fa72a3d4f6364da8f27e Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 16 Dec 2020 22:00:22 -0600
Subject: [PATCH 041/132] searchable: don't automatically include id,
created_at, updated_at.
Don't make search methods on models call super in order to search
certain default attributes (id, created_at, updated_at). Simplifies some
magic.
---
app/logical/concerns/searchable.rb | 9 +--------
app/models/artist.rb | 4 +---
app/models/artist_commentary.rb | 4 +---
app/models/artist_commentary_version.rb | 3 +--
app/models/artist_url.rb | 4 +---
app/models/artist_version.rb | 4 +---
app/models/ban.rb | 4 +---
app/models/bulk_update_request.rb | 4 +---
app/models/comment.rb | 4 +---
app/models/comment_vote.rb | 3 +--
app/models/dmail.rb | 4 +---
app/models/dtext_link.rb | 3 +--
app/models/email_address.rb | 4 +---
app/models/favorite.rb | 3 +--
app/models/favorite_group.rb | 3 +--
app/models/forum_post.rb | 3 +--
app/models/forum_post_vote.rb | 3 +--
app/models/forum_topic.rb | 3 +--
app/models/forum_topic_visit.rb | 3 +--
app/models/ip_address.rb | 3 +--
app/models/ip_ban.rb | 3 +--
app/models/mod_action.rb | 4 +---
app/models/moderation_report.rb | 3 +--
app/models/note.rb | 4 +---
app/models/note_version.rb | 4 +---
app/models/pixiv_ugoira_frame_data.rb | 3 +--
app/models/pool.rb | 4 +---
app/models/pool_version.rb | 3 +--
app/models/post.rb | 14 +++++++-------
app/models/post_appeal.rb | 3 +--
app/models/post_approval.rb | 2 +-
app/models/post_disapproval.rb | 4 +---
app/models/post_flag.rb | 4 +---
app/models/post_replacement.rb | 3 +--
app/models/post_version.rb | 3 +--
app/models/post_vote.rb | 3 +--
app/models/saved_search.rb | 3 +--
app/models/tag.rb | 4 +---
app/models/tag_relationship.rb | 3 +--
app/models/upload.rb | 4 +---
app/models/user.rb | 4 +---
app/models/user_feedback.rb | 4 +---
app/models/user_name_change_request.rb | 3 +--
app/models/wiki_page.rb | 4 +---
app/models/wiki_page_version.rb | 4 +---
45 files changed, 51 insertions(+), 121 deletions(-)
diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index 4b64261eb..321d89a81 100644
--- a/app/logical/concerns/searchable.rb
+++ b/app/logical/concerns/searchable.rb
@@ -155,6 +155,7 @@ module Searchable
indifferent_params = params.try(:with_indifferent_access) || params.try(:to_unsafe_h)
raise ArgumentError, "unable to process params" if indifferent_params.nil?
+ attributes += searchable_includes
attributes.reduce(all) do |relation, attribute|
relation.search_attribute(attribute, indifferent_params, CurrentUser.user)
end
@@ -406,14 +407,6 @@ module Searchable
where(id: ids).order(Arel.sql(order_clause.join(', ')))
end
- def search(params = {})
- params ||= {}
-
- default_attributes = (attribute_names.map(&:to_sym) & %i[id created_at updated_at])
- all_attributes = default_attributes + searchable_includes
- search_attributes(params, *all_attributes)
- end
-
private
def qualified_column_for(attr)
diff --git a/app/models/artist.rb b/app/models/artist.rb
index 6a2222c65..9ee09c407 100644
--- a/app/models/artist.rb
+++ b/app/models/artist.rb
@@ -250,9 +250,7 @@ class Artist < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names)
if params[:any_other_name_like]
q = q.any_other_name_like(params[:any_other_name_like])
diff --git a/app/models/artist_commentary.rb b/app/models/artist_commentary.rb
index d94939d22..1ea297c62 100644
--- a/app/models/artist_commentary.rb
+++ b/app/models/artist_commentary.rb
@@ -31,9 +31,7 @@ class ArtistCommentary < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
diff --git a/app/models/artist_commentary_version.rb b/app/models/artist_commentary_version.rb
index 1bf34aa02..c9fcb9a49 100644
--- a/app/models/artist_commentary_version.rb
+++ b/app/models/artist_commentary_version.rb
@@ -12,8 +12,7 @@ class ArtistCommentaryVersion < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb
index bab5e04c3..bd17c7358 100644
--- a/app/models/artist_url.rb
+++ b/app/models/artist_url.rb
@@ -40,9 +40,7 @@ class ArtistUrl < ApplicationRecord
end
def self.search(params = {})
- q = super
-
- q = q.search_attributes(params, :url, :normalized_url, :is_active)
+ q = search_attributes(params, :id, :created_at, :updated_at, :url, :normalized_url, :is_active)
q = q.url_matches(params[:url_matches])
q = q.normalized_url_matches(params[:normalized_url_matches])
diff --git a/app/models/artist_version.rb b/app/models/artist_version.rb
index 19d608e6b..c7af5690a 100644
--- a/app/models/artist_version.rb
+++ b/app/models/artist_version.rb
@@ -7,9 +7,7 @@ class ArtistVersion < ApplicationRecord
module SearchMethods
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names)
q = q.text_attribute_matches(:name, params[:name_matches])
q = q.text_attribute_matches(:group_name, params[:group_name_matches])
diff --git a/app/models/ban.rb b/app/models/ban.rb
index 4d91d04ae..6417ba739 100644
--- a/app/models/ban.rb
+++ b/app/models/ban.rb
@@ -20,9 +20,7 @@ class Ban < ApplicationRecord
end
def self.search(params)
- q = super
-
- q = q.search_attributes(params, :expires_at, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :expires_at, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q = q.expired if params[:expired].to_s.truthy?
diff --git a/app/models/bulk_update_request.rb b/app/models/bulk_update_request.rb
index b42539011..a189fe72a 100644
--- a/app/models/bulk_update_request.rb
+++ b/app/models/bulk_update_request.rb
@@ -31,9 +31,7 @@ class BulkUpdateRequest < ApplicationRecord
end
def search(params = {})
- q = super
-
- q = q.search_attributes(params, :script, :tags)
+ q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags)
q = q.text_attribute_matches(:script, params[:script_matches])
if params[:status].present?
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 0c4fd1f8d..71f23b698 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -26,9 +26,7 @@ class Comment < ApplicationRecord
module SearchMethods
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
case params[:order]
diff --git a/app/models/comment_vote.rb b/app/models/comment_vote.rb
index e44818f63..bb1fa3ca8 100644
--- a/app/models/comment_vote.rb
+++ b/app/models/comment_vote.rb
@@ -19,8 +19,7 @@ class CommentVote < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score)
q.apply_default_order(params)
end
diff --git a/app/models/dmail.rb b/app/models/dmail.rb
index e3885c236..a5c945f89 100644
--- a/app/models/dmail.rb
+++ b/app/models/dmail.rb
@@ -98,9 +98,7 @@ class Dmail < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_read, :is_deleted, :title, :body)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_read, :is_deleted, :title, :body)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index)
diff --git a/app/models/dtext_link.rb b/app/models/dtext_link.rb
index ea4902d53..9be3cc6b3 100644
--- a/app/models/dtext_link.rb
+++ b/app/models/dtext_link.rb
@@ -30,8 +30,7 @@ class DtextLink < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :link_type, :link_target)
+ q = search_attributes(params, :id, :created_at, :updated_at, :link_type, :link_target)
q.apply_default_order(params)
end
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index 9d4dd6f22..ffe6175f1 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -38,9 +38,7 @@ class EmailAddress < ApplicationRecord
end
def self.search(params)
- q = super
-
- q = q.search_attributes(params, :user, :address, :normalized_address, :is_verified, :is_deliverable)
+ q = search_attributes(params, :id, :created_at, :updated_at, :user, :address, :normalized_address, :is_verified, :is_deliverable)
q = q.apply_default_order(params)
q
diff --git a/app/models/favorite.rb b/app/models/favorite.rb
index a5488f3fd..f6ce651bd 100644
--- a/app/models/favorite.rb
+++ b/app/models/favorite.rb
@@ -12,8 +12,7 @@ class Favorite < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :post)
+ q = search_attributes(params, :id, :post)
if params[:user_id].present?
q = q.for_user(params[:user_id])
diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb
index e34ef706b..45040b79f 100644
--- a/app/models/favorite_group.rb
+++ b/app/models/favorite_group.rb
@@ -26,8 +26,7 @@ class FavoriteGroup < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :name, :is_public, :post_ids)
+ q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids)
if params[:name_matches].present?
q = q.name_matches(params[:name_matches])
diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb
index 2558a33ce..0d05876b2 100644
--- a/app/models/forum_post.rb
+++ b/app/models/forum_post.rb
@@ -41,8 +41,7 @@ class ForumPost < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :is_deleted, :body)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :body)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
if params[:linked_to].present?
diff --git a/app/models/forum_post_vote.rb b/app/models/forum_post_vote.rb
index 3ea592389..7e36cd879 100644
--- a/app/models/forum_post_vote.rb
+++ b/app/models/forum_post_vote.rb
@@ -19,8 +19,7 @@ class ForumPostVote < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score)
q = q.forum_post_matches(params[:forum_post])
q.apply_default_order(params)
end
diff --git a/app/models/forum_topic.rb b/app/models/forum_topic.rb
index f44a727d6..f70a02af6 100644
--- a/app/models/forum_topic.rb
+++ b/app/models/forum_topic.rb
@@ -86,8 +86,7 @@ class ForumTopic < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count)
q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index)
if params[:is_private].to_s.truthy?
diff --git a/app/models/forum_topic_visit.rb b/app/models/forum_topic_visit.rb
index 531f37f39..8fe1ac8ac 100644
--- a/app/models/forum_topic_visit.rb
+++ b/app/models/forum_topic_visit.rb
@@ -7,8 +7,7 @@ class ForumTopicVisit < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :user, :forum_topic_id, :last_read_at)
+ q = search_attributes(params, :id, :created_at, :updated_at, :user, :forum_topic_id, :last_read_at)
q.apply_default_order(params)
end
end
diff --git a/app/models/ip_address.rb b/app/models/ip_address.rb
index 322de5167..0e286f7f4 100644
--- a/app/models/ip_address.rb
+++ b/app/models/ip_address.rb
@@ -12,8 +12,7 @@ class IpAddress < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :ip_addr)
+ q = search_attributes(params, :ip_addr)
q.order(created_at: :desc)
end
diff --git a/app/models/ip_ban.rb b/app/models/ip_ban.rb
index 76b3b7c05..1182bef54 100644
--- a/app/models/ip_ban.rb
+++ b/app/models/ip_ban.rb
@@ -25,8 +25,7 @@ class IpBan < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:ip_addr].present?
diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb
index 87fbfda97..7d0b73615 100644
--- a/app/models/mod_action.rb
+++ b/app/models/mod_action.rb
@@ -61,9 +61,7 @@ class ModAction < ApplicationRecord
end
def self.search(params)
- q = super
-
- q = q.search_attributes(params, :category, :description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :category, :description)
q = q.text_attribute_matches(:description, params[:description_matches])
q.apply_default_order(params)
diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb
index 4fa1f77de..9916154cd 100644
--- a/app/models/moderation_report.rb
+++ b/app/models/moderation_report.rb
@@ -82,8 +82,7 @@ class ModerationReport < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)
diff --git a/app/models/note.rb b/app/models/note.rb
index 3cb745da7..431161e5f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -14,9 +14,7 @@ class Note < ApplicationRecord
module SearchMethods
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
q.apply_default_order(params)
diff --git a/app/models/note_version.rb b/app/models/note_version.rb
index d8c3aed07..310e0ef16 100644
--- a/app/models/note_version.rb
+++ b/app/models/note_version.rb
@@ -4,9 +4,7 @@ class NoteVersion < ApplicationRecord
belongs_to_updater :counter_cache => "note_update_count"
def self.search(params)
- q = super
-
- q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
diff --git a/app/models/pixiv_ugoira_frame_data.rb b/app/models/pixiv_ugoira_frame_data.rb
index 92c1eba2b..8c35a9387 100644
--- a/app/models/pixiv_ugoira_frame_data.rb
+++ b/app/models/pixiv_ugoira_frame_data.rb
@@ -13,8 +13,7 @@ class PixivUgoiraFrameData < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :data, :content_type)
+ q = search_attributes(params, :id, :data, :content_type)
q.apply_default_order(params)
end
diff --git a/app/models/pool.rb b/app/models/pool.rb
index 4c3f59e9d..547c8e295 100644
--- a/app/models/pool.rb
+++ b/app/models/pool.rb
@@ -36,9 +36,7 @@ class Pool < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_deleted, :name, :description, :post_ids)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :name, :description, :post_ids)
q = q.text_attribute_matches(:description, params[:description_matches])
if params[:post_tags_match]
diff --git a/app/models/pool_version.rb b/app/models/pool_version.rb
index 1bf6f3cb6..671f7c982 100644
--- a/app/models/pool_version.rb
+++ b/app/models/pool_version.rb
@@ -32,8 +32,7 @@ class PoolVersion < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :pool_id, :post_ids, :added_post_ids, :removed_post_ids, :updater_id, :description, :description_changed, :name, :name_changed, :version, :is_active, :is_deleted, :category)
+ q = search_attributes(params, :id, :created_at, :updated_at, :pool_id, :post_ids, :added_post_ids, :removed_post_ids, :updater_id, :description, :description_changed, :name, :name_changed, :version, :is_active, :is_deleted, :category)
if params[:post_id]
q = q.for_post_id(params[:post_id].to_i)
diff --git a/app/models/post.rb b/app/models/post.rb
index 6f54da31c..e4f1b5afe 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1289,14 +1289,14 @@ class Post < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(
+ q = search_attributes(
params,
- :rating, :source, :pixiv_id, :fav_count, :score, :up_score, :down_score, :md5, :file_ext,
- :file_size, :image_width, :image_height, :tag_count, :has_children, :has_active_children,
- :is_note_locked, :is_rating_locked, :is_status_locked, :is_pending, :is_flagged, :is_deleted,
- :is_banned, :last_comment_bumped_at, :last_commented_at, :last_noted_at
+ :id, :created_at, :updated_at, :rating, :source, :pixiv_id, :fav_count,
+ :score, :up_score, :down_score, :md5, :file_ext, :file_size, :image_width,
+ :image_height, :tag_count, :has_children, :has_active_children,
+ :is_note_locked, :is_rating_locked, :is_status_locked, :is_pending,
+ :is_flagged, :is_deleted, :is_banned, :last_comment_bumped_at,
+ :last_commented_at, :last_noted_at, :uploader_ip_addr
)
if params[:tags].present?
diff --git a/app/models/post_appeal.rb b/app/models/post_appeal.rb
index af54ce997..e8049e88c 100644
--- a/app/models/post_appeal.rb
+++ b/app/models/post_appeal.rb
@@ -17,8 +17,7 @@ class PostAppeal < ApplicationRecord
module SearchMethods
def search(params)
- q = super
- q = q.search_attributes(params, :reason, :status)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)
diff --git a/app/models/post_approval.rb b/app/models/post_approval.rb
index 599cd1ba9..127d49958 100644
--- a/app/models/post_approval.rb
+++ b/app/models/post_approval.rb
@@ -38,7 +38,7 @@ class PostApproval < ApplicationRecord
end
def self.search(params)
- q = super
+ q = search_attributes(params, :id, :created_at, :updated_at, :post, :user)
q.apply_default_order(params)
end
diff --git a/app/models/post_disapproval.rb b/app/models/post_disapproval.rb
index 08fac4921..f08d318c9 100644
--- a/app/models/post_disapproval.rb
+++ b/app/models/post_disapproval.rb
@@ -21,9 +21,7 @@ class PostDisapproval < ApplicationRecord
concerning :SearchMethods do
class_methods do
def search(params)
- q = super
-
- q = q.search_attributes(params, :message, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :message, :reason)
q = q.text_attribute_matches(:message, params[:message_matches])
q = q.with_message if params[:has_message].to_s.truthy?
diff --git a/app/models/post_flag.rb b/app/models/post_flag.rb
index 24493ba2f..3254f073c 100644
--- a/app/models/post_flag.rb
+++ b/app/models/post_flag.rb
@@ -56,9 +56,7 @@ class PostFlag < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :reason, :status)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:creator_id].present?
diff --git a/app/models/post_replacement.rb b/app/models/post_replacement.rb
index 35dd266a6..04e80a78d 100644
--- a/app/models/post_replacement.rb
+++ b/app/models/post_replacement.rb
@@ -21,8 +21,7 @@ class PostReplacement < ApplicationRecord
concerning :Search do
class_methods do
def search(params = {})
- q = super
- q = q.search_attributes(params, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url)
+ q = search_attributes(params, :id, :created_at, :updated_at, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url)
q.apply_default_order(params)
end
end
diff --git a/app/models/post_version.rb b/app/models/post_version.rb
index 59bd38b83..3cb63ecb9 100644
--- a/app/models/post_version.rb
+++ b/app/models/post_version.rb
@@ -38,8 +38,7 @@ class PostVersion < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :updater_id, :post_id, :tags, :added_tags, :removed_tags, :rating, :rating_changed, :parent_id, :parent_changed, :source, :source_changed, :version)
+ q = search_attributes(params, :id, :updated_at, :updater_id, :post_id, :tags, :added_tags, :removed_tags, :rating, :rating_changed, :parent_id, :parent_changed, :source, :source_changed, :version)
if params[:changed_tags]
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))
diff --git a/app/models/post_vote.rb b/app/models/post_vote.rb
index fdc9aa896..096b123df 100644
--- a/app/models/post_vote.rb
+++ b/app/models/post_vote.rb
@@ -19,8 +19,7 @@ class PostVote < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score)
q.apply_default_order(params)
end
diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb
index 8ab9aba52..e3dad2dc0 100644
--- a/app/models/saved_search.rb
+++ b/app/models/saved_search.rb
@@ -113,8 +113,7 @@ class SavedSearch < ApplicationRecord
concerning :Search do
class_methods do
def search(params)
- q = super
- q = q.search_attributes(params, :query)
+ q = search_attributes(params, :id, :created_at, :updated_at, :query)
if params[:label]
q = q.labeled(params[:label])
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 3c3a3bf65..86fd3fb1e 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -271,9 +271,7 @@ class Tag < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :is_locked, :category, :post_count, :name)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name)
if params[:fuzzy_name_matches].present?
q = q.fuzzy_name_matches(params[:fuzzy_name_matches])
diff --git a/app/models/tag_relationship.rb b/app/models/tag_relationship.rb
index 341f3182d..1d303b6e7 100644
--- a/app/models/tag_relationship.rb
+++ b/app/models/tag_relationship.rb
@@ -66,8 +66,7 @@ class TagRelationship < ApplicationRecord
end
def search(params)
- q = super
- q = q.search_attributes(params, :antecedent_name, :consequent_name)
+ q = search_attributes(params, :id, :created_at, :updated_at, :antecedent_name, :consequent_name)
if params[:name_matches].present?
q = q.name_matches(params[:name_matches])
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 04c61a078..383c59a65 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -182,9 +182,7 @@ class Upload < ApplicationRecord
end
def self.search(params)
- q = super
-
- q = q.search_attributes(params, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url)
+ q = search_attributes(params, :id, :created_at, :updated_at, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url)
if params[:source_matches].present?
q = q.where_like(:source, params[:source_matches])
diff --git a/app/models/user.rb b/app/models/user.rb
index 7ac2d8aea..ee0603a16 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -560,12 +560,10 @@ class User < ApplicationRecord
module SearchMethods
def search(params)
- q = super
-
params = params.dup
params[:name_matches] = params.delete(:name) if params[:name].present?
- q = q.search_attributes(params, :name, :level, :post_upload_count, :post_update_count, :note_update_count, :favorite_count)
+ q = search_attributes(params, :id, :created_at, :updated_at, :name, :level, :post_upload_count, :post_update_count, :note_update_count, :favorite_count)
if params[:name_matches].present?
q = q.where_ilike(:name, normalize_name(params[:name_matches]))
diff --git a/app/models/user_feedback.rb b/app/models/user_feedback.rb
index eaf24fcf4..f52d0d856 100644
--- a/app/models/user_feedback.rb
+++ b/app/models/user_feedback.rb
@@ -30,9 +30,7 @@ class UserFeedback < ApplicationRecord
end
def search(params)
- q = super
-
- q = q.search_attributes(params, :category, :body, :is_deleted)
+ q = search_attributes(params, :id, :created_at, :updated_at, :category, :body, :is_deleted)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
diff --git a/app/models/user_name_change_request.rb b/app/models/user_name_change_request.rb
index c3261e000..9b6b69e2b 100644
--- a/app/models/user_name_change_request.rb
+++ b/app/models/user_name_change_request.rb
@@ -19,8 +19,7 @@ class UserNameChangeRequest < ApplicationRecord
end
def self.search(params)
- q = super
- q = q.search_attributes(params, :user, :original_name, :desired_name)
+ q = search_attributes(params, :id, :created_at, :updated_at, :user, :original_name, :desired_name)
q.apply_default_order(params)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 603ddcecd..f92a0936e 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -65,9 +65,7 @@ class WikiPage < ApplicationRecord
end
def search(params = {})
- q = super
-
- q = q.search_attributes(params, :is_locked, :is_deleted, :body, :title, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :is_deleted, :body, :title, :other_names)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index, ts_config: "danbooru")
if params[:title_normalize].present?
diff --git a/app/models/wiki_page_version.rb b/app/models/wiki_page_version.rb
index b0687b191..264474951 100644
--- a/app/models/wiki_page_version.rb
+++ b/app/models/wiki_page_version.rb
@@ -7,9 +7,7 @@ class WikiPageVersion < ApplicationRecord
module SearchMethods
def search(params)
- q = super
-
- q = q.search_attributes(params, :title, :body, :other_names, :is_locked, :is_deleted)
+ q = search_attributes(params, :id, :created_at, :updated_at, :title, :body, :other_names, :is_locked, :is_deleted)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:body_matches])
From ee4516f5fe6e665e98d64a0bdde48208a59d4afb Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 16 Dec 2020 22:43:18 -0600
Subject: [PATCH 042/132] searchable: refactor searchable_includes.
Pass searchable associations directly to search_attributes instead of
defining them separately in searchable_includes.
---
app/logical/concerns/searchable.rb | 1 -
app/models/application_record.rb | 4 ----
app/models/artist.rb | 6 +-----
app/models/artist_commentary.rb | 6 +-----
app/models/artist_commentary_version.rb | 6 +-----
app/models/artist_url.rb | 6 +-----
app/models/artist_version.rb | 6 +-----
app/models/ban.rb | 6 +-----
app/models/bulk_update_request.rb | 6 +-----
app/models/comment.rb | 6 +-----
app/models/comment_vote.rb | 6 +-----
app/models/dmail.rb | 6 +-----
app/models/dtext_link.rb | 6 +-----
app/models/favorite_group.rb | 6 +-----
app/models/forum_post.rb | 6 +-----
app/models/forum_post_vote.rb | 6 +-----
app/models/forum_topic.rb | 6 +-----
app/models/ip_address.rb | 6 +-----
app/models/ip_ban.rb | 6 +-----
app/models/mod_action.rb | 6 +-----
app/models/moderation_report.rb | 6 +-----
app/models/note.rb | 6 +-----
app/models/note_version.rb | 6 +-----
app/models/pixiv_ugoira_frame_data.rb | 6 +-----
app/models/post.rb | 9 ++++-----
app/models/post_appeal.rb | 6 +-----
app/models/post_approval.rb | 6 +-----
app/models/post_disapproval.rb | 6 +-----
app/models/post_flag.rb | 6 +-----
app/models/post_replacement.rb | 6 +-----
app/models/post_vote.rb | 6 +-----
app/models/tag.rb | 6 +-----
app/models/tag_relationship.rb | 6 +-----
app/models/upload.rb | 6 +-----
app/models/user.rb | 13 ++++++++-----
app/models/user_feedback.rb | 6 +-----
app/models/wiki_page.rb | 6 +-----
app/models/wiki_page_version.rb | 6 +-----
38 files changed, 46 insertions(+), 185 deletions(-)
diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index 321d89a81..cac920cc8 100644
--- a/app/logical/concerns/searchable.rb
+++ b/app/logical/concerns/searchable.rb
@@ -155,7 +155,6 @@ module Searchable
indifferent_params = params.try(:with_indifferent_access) || params.try(:to_unsafe_h)
raise ArgumentError, "unable to process params" if indifferent_params.nil?
- attributes += searchable_includes
attributes.reduce(all) do |relation, attribute|
relation.search_attribute(attribute, indifferent_params, CurrentUser.user)
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index c65ae40d1..e162b39dd 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -93,10 +93,6 @@ class ApplicationRecord < ActiveRecord::Base
concerning :SearchMethods do
class_methods do
- def searchable_includes
- []
- end
-
def model_restriction(table)
table.project(1)
end
diff --git a/app/models/artist.rb b/app/models/artist.rb
index 9ee09c407..56f1baee8 100644
--- a/app/models/artist.rb
+++ b/app/models/artist.rb
@@ -250,7 +250,7 @@ class Artist < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names, :urls, :wiki_page, :tag_alias, :tag)
if params[:any_other_name_like]
q = q.any_other_name_like(params[:any_other_name_like])
@@ -295,10 +295,6 @@ class Artist < ApplicationRecord
super.where(table[:is_deleted].eq(false))
end
- def self.searchable_includes
- [:urls, :wiki_page, :tag_alias, :tag]
- end
-
def self.available_includes
[:members, :urls, :wiki_page, :tag_alias, :tag]
end
diff --git a/app/models/artist_commentary.rb b/app/models/artist_commentary.rb
index 1ea297c62..d1b5bcee6 100644
--- a/app/models/artist_commentary.rb
+++ b/app/models/artist_commentary.rb
@@ -31,7 +31,7 @@ class ArtistCommentary < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
@@ -144,10 +144,6 @@ class ArtistCommentary < ApplicationRecord
extend SearchMethods
include VersionMethods
- def self.searchable_includes
- [:post]
- end
-
def self.available_includes
[:post]
end
diff --git a/app/models/artist_commentary_version.rb b/app/models/artist_commentary_version.rb
index c9fcb9a49..0b4c00436 100644
--- a/app/models/artist_commentary_version.rb
+++ b/app/models/artist_commentary_version.rb
@@ -12,7 +12,7 @@ class ArtistCommentaryVersion < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post, :updater)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
@@ -55,10 +55,6 @@ class ArtistCommentaryVersion < ApplicationRecord
self[field].strip.empty? && (previous.nil? || previous[field].strip.empty?)
end
- def self.searchable_includes
- [:post, :updater]
- end
-
def self.available_includes
[:post, :updater]
end
diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb
index bd17c7358..fa9b359fb 100644
--- a/app/models/artist_url.rb
+++ b/app/models/artist_url.rb
@@ -40,7 +40,7 @@ class ArtistUrl < ApplicationRecord
end
def self.search(params = {})
- q = search_attributes(params, :id, :created_at, :updated_at, :url, :normalized_url, :is_active)
+ q = search_attributes(params, :id, :created_at, :updated_at, :url, :normalized_url, :is_active, :artist)
q = q.url_matches(params[:url_matches])
q = q.normalized_url_matches(params[:normalized_url_matches])
@@ -126,10 +126,6 @@ class ArtistUrl < ApplicationRecord
errors.add(:url, "'#{uri}' is malformed: #{error}")
end
- def self.searchable_includes
- [:artist]
- end
-
def self.available_includes
[:artist]
end
diff --git a/app/models/artist_version.rb b/app/models/artist_version.rb
index c7af5690a..a743c43ed 100644
--- a/app/models/artist_version.rb
+++ b/app/models/artist_version.rb
@@ -7,7 +7,7 @@ class ArtistVersion < ApplicationRecord
module SearchMethods
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names, :updater, :artist)
q = q.text_attribute_matches(:name, params[:name_matches])
q = q.text_attribute_matches(:group_name, params[:group_name_matches])
@@ -103,10 +103,6 @@ class ArtistVersion < ApplicationRecord
end
end
- def self.searchable_includes
- [:updater, :artist]
- end
-
def self.available_includes
[:updater, :artist]
end
diff --git a/app/models/ban.rb b/app/models/ban.rb
index 6417ba739..c641b4d5a 100644
--- a/app/models/ban.rb
+++ b/app/models/ban.rb
@@ -20,7 +20,7 @@ class Ban < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :expires_at, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :expires_at, :reason, :user, :banner)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q = q.expired if params[:expired].to_s.truthy?
@@ -87,10 +87,6 @@ class Ban < ApplicationRecord
ModAction.log(%{Unbanned <@#{user_name}>}, :user_unban)
end
- def self.searchable_includes
- [:user, :banner]
- end
-
def self.available_includes
[:user, :banner]
end
diff --git a/app/models/bulk_update_request.rb b/app/models/bulk_update_request.rb
index a189fe72a..d7afa226d 100644
--- a/app/models/bulk_update_request.rb
+++ b/app/models/bulk_update_request.rb
@@ -31,7 +31,7 @@ class BulkUpdateRequest < ApplicationRecord
end
def search(params = {})
- q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags)
+ q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags, :user, :forum_topic, :forum_post, :approver)
q = q.text_attribute_matches(:script, params[:script_matches])
if params[:status].present?
@@ -126,10 +126,6 @@ class BulkUpdateRequest < ApplicationRecord
status == "rejected"
end
- def self.searchable_includes
- [:user, :forum_topic, :forum_post, :approver]
- end
-
def self.available_includes
[:user, :forum_topic, :forum_post, :approver]
end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 71f23b698..2207706ef 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -26,7 +26,7 @@ class Comment < ApplicationRecord
module SearchMethods
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score, :post, :creator, :updater)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
case params[:order]
@@ -137,10 +137,6 @@ class Comment < ApplicationRecord
DText.quote(body, creator.name)
end
- def self.searchable_includes
- [:post, :creator, :updater]
- end
-
def self.available_includes
[:post, :creator, :updater]
end
diff --git a/app/models/comment_vote.rb b/app/models/comment_vote.rb
index bb1fa3ca8..b87642d10 100644
--- a/app/models/comment_vote.rb
+++ b/app/models/comment_vote.rb
@@ -19,7 +19,7 @@ class CommentVote < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score, :comment, :user)
q.apply_default_order(params)
end
@@ -37,10 +37,6 @@ class CommentVote < ApplicationRecord
score == -1
end
- def self.searchable_includes
- [:comment, :user]
- end
-
def self.available_includes
[:comment, :user]
end
diff --git a/app/models/dmail.rb b/app/models/dmail.rb
index a5c945f89..63094bff5 100644
--- a/app/models/dmail.rb
+++ b/app/models/dmail.rb
@@ -98,7 +98,7 @@ class Dmail < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_read, :is_deleted, :title, :body)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_read, :is_deleted, :title, :body, :to, :from)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index)
@@ -180,10 +180,6 @@ class Dmail < ApplicationRecord
key ? "dmail ##{id}/#{self.key}" : "dmail ##{id}"
end
- def self.searchable_includes
- [:to, :from]
- end
-
def self.available_includes
[:owner, :to, :from]
end
diff --git a/app/models/dtext_link.rb b/app/models/dtext_link.rb
index 9be3cc6b3..3d3f8d62b 100644
--- a/app/models/dtext_link.rb
+++ b/app/models/dtext_link.rb
@@ -30,7 +30,7 @@ class DtextLink < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :link_type, :link_target)
+ q = search_attributes(params, :id, :created_at, :updated_at, :link_type, :link_target, :model, :linked_wiki, :linked_tag)
q.apply_default_order(params)
end
@@ -48,10 +48,6 @@ class DtextLink < ApplicationRecord
where(link_type: :wiki_link)
end
- def self.searchable_includes
- [:model, :linked_wiki, :linked_tag]
- end
-
def self.available_includes
[:model, :linked_wiki, :linked_tag]
end
diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb
index 45040b79f..5a0c07190 100644
--- a/app/models/favorite_group.rb
+++ b/app/models/favorite_group.rb
@@ -26,7 +26,7 @@ class FavoriteGroup < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids)
+ q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids, :creator)
if params[:name_matches].present?
q = q.name_matches(params[:name_matches])
@@ -163,10 +163,6 @@ class FavoriteGroup < ApplicationRecord
post_ids.include?(post_id)
end
- def self.searchable_includes
- [:creator]
- end
-
def self.available_includes
[:creator]
end
diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb
index 0d05876b2..c8aad7962 100644
--- a/app/models/forum_post.rb
+++ b/app/models/forum_post.rb
@@ -41,7 +41,7 @@ class ForumPost < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :body)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :body, :creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
if params[:linked_to].present?
@@ -163,10 +163,6 @@ class ForumPost < ApplicationRecord
"forum ##{id}"
end
- def self.searchable_includes
- [:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
- end
-
def self.available_includes
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
end
diff --git a/app/models/forum_post_vote.rb b/app/models/forum_post_vote.rb
index 7e36cd879..6a2ac7926 100644
--- a/app/models/forum_post_vote.rb
+++ b/app/models/forum_post_vote.rb
@@ -19,7 +19,7 @@ class ForumPostVote < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score, :creator, :forum_post)
q = q.forum_post_matches(params[:forum_post])
q.apply_default_order(params)
end
@@ -58,10 +58,6 @@ class ForumPostVote < ApplicationRecord
end
end
- def self.searchable_includes
- [:creator, :forum_post]
- end
-
def self.available_includes
[:creator, :forum_post]
end
diff --git a/app/models/forum_topic.rb b/app/models/forum_topic.rb
index f70a02af6..0ab60d042 100644
--- a/app/models/forum_topic.rb
+++ b/app/models/forum_topic.rb
@@ -86,7 +86,7 @@ class ForumTopic < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count, :creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications)
q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index)
if params[:is_private].to_s.truthy?
@@ -189,10 +189,6 @@ class ForumTopic < ApplicationRecord
title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "")
end
- def self.searchable_includes
- [:creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications]
- end
-
def self.available_includes
[:creator, :updater, :original_post]
end
diff --git a/app/models/ip_address.rb b/app/models/ip_address.rb
index 0e286f7f4..77f44b3ff 100644
--- a/app/models/ip_address.rb
+++ b/app/models/ip_address.rb
@@ -12,7 +12,7 @@ class IpAddress < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :ip_addr)
+ q = search_attributes(params, :ip_addr, :user, :model)
q.order(created_at: :desc)
end
@@ -49,10 +49,6 @@ class IpAddress < ApplicationRecord
true
end
- def self.searchable_includes
- [:user, :model]
- end
-
def self.available_includes
[:user, :model]
end
diff --git a/app/models/ip_ban.rb b/app/models/ip_ban.rb
index 1182bef54..223539f43 100644
--- a/app/models/ip_ban.rb
+++ b/app/models/ip_ban.rb
@@ -25,7 +25,7 @@ class IpBan < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:ip_addr].present?
@@ -77,10 +77,6 @@ class IpBan < ApplicationRecord
super(ip_addr.strip)
end
- def self.searchable_includes
- [:creator]
- end
-
def self.available_includes
[:creator]
end
diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb
index 7d0b73615..437d3efea 100644
--- a/app/models/mod_action.rb
+++ b/app/models/mod_action.rb
@@ -61,7 +61,7 @@ class ModAction < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :category, :description)
+ q = search_attributes(params, :id, :created_at, :updated_at, :category, :description, :creator)
q = q.text_attribute_matches(:description, params[:description_matches])
q.apply_default_order(params)
@@ -75,10 +75,6 @@ class ModAction < ApplicationRecord
create(creator: user, description: desc, category: categories[cat])
end
- def self.searchable_includes
- [:creator]
- end
-
def self.available_includes
[:creator]
end
diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb
index 9916154cd..20874e2d1 100644
--- a/app/models/moderation_report.rb
+++ b/app/models/moderation_report.rb
@@ -82,16 +82,12 @@ class ModerationReport < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)
end
- def self.searchable_includes
- [:creator, :model]
- end
-
def self.available_includes
[:creator, :model]
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 431161e5f..f2cc46ed3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -14,7 +14,7 @@ class Note < ApplicationRecord
module SearchMethods
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :post)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
q.apply_default_order(params)
@@ -127,10 +127,6 @@ class Note < ApplicationRecord
new_note.save
end
- def self.searchable_includes
- [:post]
- end
-
def self.available_includes
[:post]
end
diff --git a/app/models/note_version.rb b/app/models/note_version.rb
index 310e0ef16..2b973650f 100644
--- a/app/models/note_version.rb
+++ b/app/models/note_version.rb
@@ -4,7 +4,7 @@ class NoteVersion < ApplicationRecord
belongs_to_updater :counter_cache => "note_update_count"
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :updater, :note, :post)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
@@ -69,10 +69,6 @@ class NoteVersion < ApplicationRecord
end
end
- def self.searchable_includes
- [:updater, :note, :post]
- end
-
def self.available_includes
[:updater, :note, :post]
end
diff --git a/app/models/pixiv_ugoira_frame_data.rb b/app/models/pixiv_ugoira_frame_data.rb
index 8c35a9387..bbb5a5498 100644
--- a/app/models/pixiv_ugoira_frame_data.rb
+++ b/app/models/pixiv_ugoira_frame_data.rb
@@ -4,16 +4,12 @@ class PixivUgoiraFrameData < ApplicationRecord
serialize :data
before_validation :normalize_data, on: :create
- def self.searchable_includes
- [:post]
- end
-
def self.available_includes
[:post]
end
def self.search(params)
- q = search_attributes(params, :id, :data, :content_type)
+ q = search_attributes(params, :id, :data, :content_type, :post)
q.apply_default_order(params)
end
diff --git a/app/models/post.rb b/app/models/post.rb
index e4f1b5afe..9fd75b7ad 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1296,7 +1296,10 @@ class Post < ApplicationRecord
:image_height, :tag_count, :has_children, :has_active_children,
:is_note_locked, :is_rating_locked, :is_status_locked, :is_pending,
:is_flagged, :is_deleted, :is_banned, :last_comment_bumped_at,
- :last_commented_at, :last_noted_at, :uploader_ip_addr
+ :last_commented_at, :last_noted_at, :uploader_ip_addr,
+ :uploader, :updater, :approver, :parent, :upload, :artist_commentary,
+ :flags, :appeals, :notes, :comments, :children, :approvals,
+ :replacements, :pixiv_ugoira_frame_data
)
if params[:tags].present?
@@ -1499,10 +1502,6 @@ class Post < ApplicationRecord
super.where(table[:is_pending].eq(false)).where(table[:is_flagged].eq(false)).where(table[:is_deleted].eq(false))
end
- def self.searchable_includes
- [:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
- end
-
def self.available_includes
[:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
end
diff --git a/app/models/post_appeal.rb b/app/models/post_appeal.rb
index e8049e88c..e7ce3d069 100644
--- a/app/models/post_appeal.rb
+++ b/app/models/post_appeal.rb
@@ -17,7 +17,7 @@ class PostAppeal < ApplicationRecord
module SearchMethods
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :creator, :post)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)
@@ -34,10 +34,6 @@ class PostAppeal < ApplicationRecord
errors.add(:post, "cannot be appealed") if post.is_status_locked? || !post.is_appealable?
end
- def self.searchable_includes
- [:creator, :post]
- end
-
def self.available_includes
[:creator, :post]
end
diff --git a/app/models/post_approval.rb b/app/models/post_approval.rb
index 127d49958..1b95b619c 100644
--- a/app/models/post_approval.rb
+++ b/app/models/post_approval.rb
@@ -38,14 +38,10 @@ class PostApproval < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :post, :user)
+ q = search_attributes(params, :id, :created_at, :updated_at, :user, :post)
q.apply_default_order(params)
end
- def self.searchable_includes
- [:user, :post]
- end
-
def self.available_includes
[:user, :post]
end
diff --git a/app/models/post_disapproval.rb b/app/models/post_disapproval.rb
index f08d318c9..eccf6f41c 100644
--- a/app/models/post_disapproval.rb
+++ b/app/models/post_disapproval.rb
@@ -21,7 +21,7 @@ class PostDisapproval < ApplicationRecord
concerning :SearchMethods do
class_methods do
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :message, :reason)
+ q = search_attributes(params, :id, :created_at, :updated_at, :message, :reason, :user, :post)
q = q.text_attribute_matches(:message, params[:message_matches])
q = q.with_message if params[:has_message].to_s.truthy?
@@ -39,10 +39,6 @@ class PostDisapproval < ApplicationRecord
end
end
- def self.searchable_includes
- [:user, :post]
- end
-
def self.available_includes
[:user, :post]
end
diff --git a/app/models/post_flag.rb b/app/models/post_flag.rb
index 3254f073c..647d852ed 100644
--- a/app/models/post_flag.rb
+++ b/app/models/post_flag.rb
@@ -56,7 +56,7 @@ class PostFlag < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status)
+ q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :post)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:creator_id].present?
@@ -111,10 +111,6 @@ class PostFlag < ApplicationRecord
post.uploader_id
end
- def self.searchable_includes
- [:post]
- end
-
def self.available_includes
[:post]
end
diff --git a/app/models/post_replacement.rb b/app/models/post_replacement.rb
index 04e80a78d..ae5a23174 100644
--- a/app/models/post_replacement.rb
+++ b/app/models/post_replacement.rb
@@ -21,7 +21,7 @@ class PostReplacement < ApplicationRecord
concerning :Search do
class_methods do
def search(params = {})
- q = search_attributes(params, :id, :created_at, :updated_at, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url)
+ q = search_attributes(params, :id, :created_at, :updated_at, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url, :creator, :post)
q.apply_default_order(params)
end
end
@@ -38,10 +38,6 @@ class PostReplacement < ApplicationRecord
tags.join(" ")
end
- def self.searchable_includes
- [:creator, :post]
- end
-
def self.available_includes
[:creator, :post]
end
diff --git a/app/models/post_vote.rb b/app/models/post_vote.rb
index 096b123df..ab8ea1090 100644
--- a/app/models/post_vote.rb
+++ b/app/models/post_vote.rb
@@ -19,7 +19,7 @@ class PostVote < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :score)
+ q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post)
q.apply_default_order(params)
end
@@ -49,10 +49,6 @@ class PostVote < ApplicationRecord
end
end
- def self.searchable_includes
- [:user, :post]
- end
-
def self.available_includes
[:user, :post]
end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 86fd3fb1e..27255bed9 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -271,7 +271,7 @@ class Tag < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links)
if params[:fuzzy_name_matches].present?
q = q.fuzzy_name_matches(params[:fuzzy_name_matches])
@@ -376,10 +376,6 @@ class Tag < ApplicationRecord
super.where(table[:post_count].gt(0))
end
- def self.searchable_includes
- [:wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links]
- end
-
def self.available_includes
[:wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links]
end
diff --git a/app/models/tag_relationship.rb b/app/models/tag_relationship.rb
index 1d303b6e7..28e6bacb1 100644
--- a/app/models/tag_relationship.rb
+++ b/app/models/tag_relationship.rb
@@ -66,7 +66,7 @@ class TagRelationship < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :antecedent_name, :consequent_name)
+ q = search_attributes(params, :id, :created_at, :updated_at, :antecedent_name, :consequent_name, :creator, :approver, :forum_post, :forum_topic, :antecedent_tag, :consequent_tag, :antecedent_wiki, :consequent_wiki)
if params[:name_matches].present?
q = q.name_matches(params[:name_matches])
@@ -125,10 +125,6 @@ class TagRelationship < ApplicationRecord
super.where(table[:status].eq("active"))
end
- def self.searchable_includes
- [:creator, :approver, :forum_post, :forum_topic, :antecedent_tag, :consequent_tag, :antecedent_wiki, :consequent_wiki]
- end
-
def self.available_includes
[:creator, :approver, :forum_post, :forum_topic, :antecedent_tag, :consequent_tag, :antecedent_wiki, :consequent_wiki]
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 383c59a65..6c2878076 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -182,7 +182,7 @@ class Upload < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url)
+ q = search_attributes(params, :id, :created_at, :updated_at, :source, :rating, :parent_id, :server, :md5, :server, :file_ext, :file_size, :image_width, :image_height, :referer_url, :uploader, :post)
if params[:source_matches].present?
q = q.where_like(:source, params[:source_matches])
@@ -223,10 +223,6 @@ class Upload < ApplicationRecord
artist_commentary_title.present? || artist_commentary_desc.present? || translated_commentary_title.present? || translated_commentary_desc.present?
end
- def self.searchable_includes
- [:uploader, :post]
- end
-
def self.available_includes
[:uploader, :post]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index ee0603a16..c56a19adb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -563,7 +563,14 @@ class User < ApplicationRecord
params = params.dup
params[:name_matches] = params.delete(:name) if params[:name].present?
- q = search_attributes(params, :id, :created_at, :updated_at, :name, :level, :post_upload_count, :post_update_count, :note_update_count, :favorite_count)
+ q = search_attributes(params,
+ :id, :created_at, :updated_at, :name, :level, :post_upload_count,
+ :post_update_count, :note_update_count, :favorite_count, :posts,
+ :note_versions, :artist_commentary_versions, :post_appeals,
+ :post_approvals, :artist_versions, :comments, :wiki_page_versions,
+ :feedback, :forum_topics, :forum_posts, :forum_post_votes,
+ :tag_aliases, :tag_implications, :bans, :inviter
+ )
if params[:name_matches].present?
q = q.where_ilike(:name, normalize_name(params[:name_matches]))
@@ -631,10 +638,6 @@ class User < ApplicationRecord
"<@#{name}>"
end
- def self.searchable_includes
- [:posts, :note_versions, :artist_commentary_versions, :post_appeals, :post_approvals, :artist_versions, :comments, :wiki_page_versions, :feedback, :forum_topics, :forum_posts, :forum_post_votes, :tag_aliases, :tag_implications, :bans, :inviter]
- end
-
def self.available_includes
[:inviter]
end
diff --git a/app/models/user_feedback.rb b/app/models/user_feedback.rb
index f52d0d856..cda52916c 100644
--- a/app/models/user_feedback.rb
+++ b/app/models/user_feedback.rb
@@ -30,7 +30,7 @@ class UserFeedback < ApplicationRecord
end
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :category, :body, :is_deleted)
+ q = search_attributes(params, :id, :created_at, :updated_at, :category, :body, :is_deleted, :creator, :user)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
@@ -56,10 +56,6 @@ class UserFeedback < ApplicationRecord
Dmail.create_automated(:to_id => user_id, :title => "Your user record has been updated", :body => body)
end
- def self.searchable_includes
- [:creator, :user]
- end
-
def self.available_includes
[:creator, :user]
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index f92a0936e..3b0b28045 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -65,7 +65,7 @@ class WikiPage < ApplicationRecord
end
def search(params = {})
- q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :is_deleted, :body, :title, :other_names)
+ q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :is_deleted, :body, :title, :other_names, :tag, :artist, :dtext_links)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index, ts_config: "danbooru")
if params[:title_normalize].present?
@@ -246,10 +246,6 @@ class WikiPage < ApplicationRecord
super.where(table[:is_deleted].eq(false))
end
- def self.searchable_includes
- [:tag, :artist, :dtext_links]
- end
-
def self.available_includes
[:tag, :artist, :dtext_links]
end
diff --git a/app/models/wiki_page_version.rb b/app/models/wiki_page_version.rb
index 264474951..1f405d4f1 100644
--- a/app/models/wiki_page_version.rb
+++ b/app/models/wiki_page_version.rb
@@ -7,7 +7,7 @@ class WikiPageVersion < ApplicationRecord
module SearchMethods
def search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :title, :body, :other_names, :is_locked, :is_deleted)
+ q = search_attributes(params, :id, :created_at, :updated_at, :title, :body, :other_names, :is_locked, :is_deleted, :updater, :wiki_page, :artist, :tag)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:body_matches])
@@ -75,10 +75,6 @@ class WikiPageVersion < ApplicationRecord
end
end
- def self.searchable_includes
- [:updater, :wiki_page, :artist, :tag]
- end
-
def self.available_includes
[:updater, :wiki_page, :artist, :tag]
end
From 7a87225ac855f51679e000205e4685db9059d85e Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 03:05:27 -0600
Subject: [PATCH 043/132] Add basic server status page at /status.
Lists versions of various dependencies plus some Postgres and Redis
metrics.
---
app/controllers/status_controller.rb | 8 ++
app/logical/server_status.rb | 109 ++++++++++++++++++++++
app/views/status/show.html.erb | 32 +++++++
config/routes.rb | 1 +
test/functional/status_controller_test.rb | 20 ++++
5 files changed, 170 insertions(+)
create mode 100644 app/controllers/status_controller.rb
create mode 100644 app/logical/server_status.rb
create mode 100644 app/views/status/show.html.erb
create mode 100644 test/functional/status_controller_test.rb
diff --git a/app/controllers/status_controller.rb b/app/controllers/status_controller.rb
new file mode 100644
index 000000000..4cd509491
--- /dev/null
+++ b/app/controllers/status_controller.rb
@@ -0,0 +1,8 @@
+class StatusController < ApplicationController
+ respond_to :html, :json, :xml
+
+ def show
+ @status = ServerStatus.new
+ respond_with(@status)
+ end
+end
diff --git a/app/logical/server_status.rb b/app/logical/server_status.rb
new file mode 100644
index 000000000..197582bf7
--- /dev/null
+++ b/app/logical/server_status.rb
@@ -0,0 +1,109 @@
+class ServerStatus
+ extend Memoist
+ include ActiveModel::Serializers::JSON
+ include ActiveModel::Serializers::Xml
+
+ def serializable_hash(*options)
+ {
+ status: {
+ hostname: hostname,
+ uptime: uptime,
+ loadavg: loadavg,
+ ruby_version: RUBY_VERSION,
+ distro_version: distro_version,
+ kernel_version: kernel_version,
+ libvips_version: libvips_version,
+ ffmpeg_version: ffmpeg_version,
+ mkvmerge_version: mkvmerge_version,
+ redis_version: redis_version,
+ postgres_version: postgres_version,
+ },
+ postgres: {
+ connection_stats: postgres_connection_stats,
+ },
+ redis: {
+ info: redis_info,
+ }
+ }
+ end
+
+ concerning :InfoMethods do
+ def hostname
+ Socket.gethostname
+ end
+
+ def uptime
+ seconds = File.read("/proc/uptime").split[0].to_f
+ "#{seconds.seconds.in_days.round} days"
+ end
+
+ def loadavg
+ File.read("/proc/loadavg").chomp
+ end
+
+ def kernel_version
+ File.read("/proc/version").chomp
+ end
+
+ def distro_version
+ `source /etc/os-release; echo "$NAME $VERSION"`.chomp
+ end
+
+ def libvips_version
+ Vips::LIBRARY_VERSION
+ end
+
+ def ffmpeg_version
+ version = `ffmpeg -version`
+ version[/ffmpeg version ([0-9.]+)/, 1]
+ end
+
+ def mkvmerge_version
+ `mkvmerge --version`.chomp
+ end
+ end
+
+ concerning :RedisMethods do
+ def redis_info
+ return {} if Rails.cache.try(:redis).nil?
+ Rails.cache.redis.info
+ end
+
+ def redis_used_memory
+ redis_info["used_memory_rss_human"]
+ end
+
+ def redis_version
+ redis_info["redis_version"]
+ end
+ end
+
+ concerning :PostgresMethods do
+ def postgres_version
+ ApplicationRecord.connection.select_value("SELECT version()")
+ end
+
+ def postgres_active_connections
+ ApplicationRecord.connection.select_value("SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'")
+ end
+
+ def postgres_connection_stats
+ run_query("SELECT pid, state, query_start, state_change, xact_start, backend_start, backend_type FROM pg_stat_activity ORDER BY state, query_start DESC, backend_type")
+ end
+
+ def run_query(query)
+ result = ApplicationRecord.connection.select_all(query)
+ serialize_result(result)
+ end
+
+ def serialize_result(result)
+ result.rows.map do |row|
+ row.each_with_index.map do |col, i|
+ [result.columns[i], col]
+ end.to_h
+ end
+ end
+ end
+
+ memoize :redis_info
+end
diff --git a/app/views/status/show.html.erb b/app/views/status/show.html.erb
new file mode 100644
index 000000000..c0f138359
--- /dev/null
+++ b/app/views/status/show.html.erb
@@ -0,0 +1,32 @@
+
+
+
Status
+
+
+
+ Server: <%= @status.hostname %>
+
+ <%= render "list", hash: @status.serializable_hash[:status] %>
+
+
+ Postgres
+
+
+
+ <%= pluralize @status.postgres_active_connections, "active connection" %>.
+
+
+ <%= render "table", rows: @status.serializable_hash[:postgres][:connection_stats] %>
+
+
+ Redis
+
+
+
+ <%= @status.redis_used_memory %> memory used.
+
+
+ <%= render "list", hash: @status.serializable_hash[:redis][:info] %>
+
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 11d7e7d28..9b916feaf 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -227,6 +227,7 @@ Rails.application.routes.draw do
get :sign_out, on: :collection
end
resource :source, :only => [:show]
+ resource :status, only: [:show], controller: "status"
resources :tags do
collection do
get :autocomplete
diff --git a/test/functional/status_controller_test.rb b/test/functional/status_controller_test.rb
new file mode 100644
index 000000000..b05467262
--- /dev/null
+++ b/test/functional/status_controller_test.rb
@@ -0,0 +1,20 @@
+require 'test_helper'
+
+class StatusControllerTest < ActionDispatch::IntegrationTest
+ context "The status controller" do
+ should "work for a html response" do
+ get status_path
+ assert_response :success
+ end
+
+ should "work for a json response" do
+ get status_path(format: :json)
+ assert_response :success
+ end
+
+ should "work for an xml response" do
+ get status_path(format: :json)
+ assert_response :success
+ end
+ end
+end
From 1809f67b2b621accbaa884a5179d6bfb5aa54101 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 13:17:55 -0600
Subject: [PATCH 044/132] tags: don't allow tags to begin with a '/'.
Disallow tags from starting with a '/' character. This is so that tag
abbreviations in autocomplete, which start with a '/', don't conflict
with regular tags.
Also disallow some other punctuation characters: `%{})]. Currently no
tags start with these characters. This is to reserve other special
characters in case we need them for other future syntax extensions.
---
app/logical/tag_name_validator.rb | 8 ++------
test/unit/autocomplete_service_test.rb | 10 ----------
test/unit/tag_test.rb | 7 +++++++
3 files changed, 9 insertions(+), 16 deletions(-)
diff --git a/app/logical/tag_name_validator.rb b/app/logical/tag_name_validator.rb
index 10c1d4b67..63690f9ec 100644
--- a/app/logical/tag_name_validator.rb
+++ b/app/logical/tag_name_validator.rb
@@ -7,12 +7,8 @@ class TagNameValidator < ActiveModel::EachValidator
record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
when /,/
record.errors.add(attribute, "'#{value}' cannot contain commas (',')")
- when /\A~/
- record.errors.add(attribute, "'#{value}' cannot begin with a tilde ('~')")
- when /\A-/
- record.errors.add(attribute, "'#{value}' cannot begin with a dash ('-')")
- when /\A_/
- record.errors.add(attribute, "'#{value}' cannot begin with an underscore")
+ when /\A[-~_`%){}\]\/]/
+ record.errors.add(attribute, "'#{value}' cannot begin with a '#{value[0]}'")
when /_\z/
record.errors.add(attribute, "'#{value}' cannot end with an underscore")
when /__/
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index de2e547b3..1abe96f80 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -97,16 +97,6 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
end
-
- should "work for regular tags starting with a /" do
- create(:tag, name: "jojo_pose", post_count: 100)
- create(:tag, name: "/jp/", post_count: 50)
-
- assert_autocomplete_equals(%w[/jp/ jojo_pose], "/", :tag_query)
- assert_autocomplete_equals(%w[/jp/ jojo_pose], "/j", :tag_query)
- assert_autocomplete_equals(%w[/jp/ jojo_pose], "/jp", :tag_query)
- assert_autocomplete_equals(%w[/jp/], "/jp/", :tag_query)
- end
end
should "autocomplete tags from wiki and artist other names" do
diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb
index ede77bb90..43a06af12 100644
--- a/test/unit/tag_test.rb
+++ b/test/unit/tag_test.rb
@@ -169,6 +169,13 @@ class TagTest < ActiveSupport::TestCase
should_not allow_value("___").for(:name).on(:create)
should_not allow_value("~foo").for(:name).on(:create)
should_not allow_value("-foo").for(:name).on(:create)
+ should_not allow_value("/foo").for(:name).on(:create)
+ should_not allow_value("`foo").for(:name).on(:create)
+ should_not allow_value("%foo").for(:name).on(:create)
+ should_not allow_value(")foo").for(:name).on(:create)
+ should_not allow_value("{foo").for(:name).on(:create)
+ should_not allow_value("}foo").for(:name).on(:create)
+ should_not allow_value("]foo").for(:name).on(:create)
should_not allow_value("_foo").for(:name).on(:create)
should_not allow_value("foo_").for(:name).on(:create)
should_not allow_value("foo__bar").for(:name).on(:create)
From 991896c4eb8e791e30f482dabf5be15800dd646f Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 13:41:19 -0600
Subject: [PATCH 045/132] tags: don't allow tags more than 170 chars long.
Limit tag length to 170 chars. 170 chars was chosen because it's
longer than the longest active tag on Danbooru.
Tag length is limited because in some contexts we can't deal with
excessively long tags. Tag autocorrect for example uses the levenshtein
function in Postgres, which can't handle strings more than 255 chars long.
---
app/logical/tag_name_validator.rb | 8 +++++++-
test/unit/tag_test.rb | 1 +
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/app/logical/tag_name_validator.rb b/app/logical/tag_name_validator.rb
index 63690f9ec..029a8c5c0 100644
--- a/app/logical/tag_name_validator.rb
+++ b/app/logical/tag_name_validator.rb
@@ -1,6 +1,12 @@
class TagNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- case Tag.normalize_name(value)
+ value = Tag.normalize_name(value)
+
+ if value.size > 170
+ record.errors.add(attribute, "'#{value}' cannot be more than 255 characters long")
+ end
+
+ case value
when /\A_*\z/
record.errors.add(attribute, "'#{value}' cannot be blank")
when /\*/
diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb
index 43a06af12..0c4bd3dce 100644
--- a/test/unit/tag_test.rb
+++ b/test/unit/tag_test.rb
@@ -185,6 +185,7 @@ class TagTest < ActiveSupport::TestCase
should_not allow_value("café").for(:name).on(:create)
should_not allow_value("東方").for(:name).on(:create)
should_not allow_value("FAV:blah").for(:name).on(:create)
+ should_not allow_value("X"*171).for(:name).on(:create)
metatags = PostQueryBuilder::METATAGS + TagCategory.mapping.keys
metatags.each do |metatag|
From 2c1da660fd7c84717e91513bbe6967ca5dcb2b6a Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 19:02:49 -0600
Subject: [PATCH 046/132] tags: allow tag abbreviations in searches and during
tagging.
Expand the tag abbreviation system introduced in b0be8ae45 so that it
works in searches and when tagging posts, not just in autocomplete.
For example, you can tag a post with /evth and it will add the tag
eyebrows_visible_through_hair. You can search for /evth and it will
search for the tag eyebrows_visible_through_hair.
Some more examples:
* /ops is short for one-piece_swimsuit
* /hooe is short for hair_over_one_eye
* /saol is short for standing_on_one_leg
* /tlozbotw is short for the_legend_of_zelda:_breath_of_the_wild
If two tags have the same abbreviation, then the larger tag takes
precedence. For example, /be is short for blue_eyes, not brown_eyes,
because blue_eyes is the bigger tag.
If there is an existing shortcut alias that conflicts with the
abbreviation, then the alias take precedence. For example, /sh is short
for suzumiya_haruhi, not short_hair, because there's an old alias for
/sh -> suzumiya_haruhi.
---
app/logical/autocomplete_service.rb | 6 +++++-
app/logical/concerns/searchable.rb | 2 +-
app/models/tag.rb | 5 +++++
app/models/tag_alias.rb | 8 ++++++++
config/initializers/core_extensions.rb | 5 +++++
test/unit/autocomplete_service_test.rb | 8 ++++++++
test/unit/post_query_builder_test.rb | 11 +++++++++++
test/unit/post_test.rb | 27 ++++++++++++++++++++++++++
test/unit/tag_alias_test.rb | 12 ++++++++++++
9 files changed, 82 insertions(+), 2 deletions(-)
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index 0c5d39f93..f75101098 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -67,9 +67,13 @@ class AutocompleteService
def autocomplete_tag(string)
if string.starts_with?("/")
string = string + "*" unless string.include?("*")
+
results = tag_matches(string)
results += tag_abbreviation_matches(string)
- results = results.sort_by { |r| [r[:antecedent].to_s.size, -r[:post_count]] }
+ results = results.sort_by do |r|
+ [r[:type] == "tag-alias" ? 0 : 1, r[:antecedent].to_s.size, -r[:post_count]]
+ end
+
results = results.uniq { |r| r[:value] }.take(limit)
elsif string.include?("*")
results = tag_matches(string)
diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index cac920cc8..a83a9c83a 100644
--- a/app/logical/concerns/searchable.rb
+++ b/app/logical/concerns/searchable.rb
@@ -53,7 +53,7 @@ module Searchable
end
def where_iequals(attr, value)
- where_ilike(attr, value.gsub(/\\/, '\\\\').gsub(/\*/, '\*'))
+ where_ilike(attr, value.escape_wildcards)
end
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 27255bed9..527f8a797 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -270,6 +270,11 @@ class Tag < ApplicationRecord
where("regexp_replace(tags.name, ?, '\\1', 'g') LIKE ?", ABBREVIATION_REGEXP.source, abbrev.to_escaped_for_sql_like)
end
+ def find_by_abbreviation(abbrev)
+ abbrev = abbrev.delete_prefix("/")
+ abbreviation_matches(abbrev.escape_wildcards).order(post_count: :desc).first
+ end
+
def search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links)
diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb
index 9c460e27a..b3cb9c4f2 100644
--- a/app/models/tag_alias.rb
+++ b/app/models/tag_alias.rb
@@ -7,7 +7,15 @@ class TagAlias < TagRelationship
def self.to_aliased(names)
names = Array(names).map(&:to_s)
return [] if names.empty?
+
aliases = active.where(antecedent_name: names).map { |ta| [ta.antecedent_name, ta.consequent_name] }.to_h
+
+ abbreviations = names.select { |name| name.starts_with?("/") && !aliases.has_key?(name) }
+ abbreviations.each do |abbrev|
+ tag = Tag.nonempty.find_by_abbreviation(abbrev)
+ aliases[abbrev] = tag.name if tag.present?
+ end
+
names.map { |name| aliases[name] || name }
end
diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb
index 61f85f9d0..ef7dba48d 100644
--- a/config/initializers/core_extensions.rb
+++ b/config/initializers/core_extensions.rb
@@ -16,6 +16,11 @@ module Danbooru
string
end
+ # escape \ and * characters so that they're treated literally in LIKE searches.
+ def escape_wildcards
+ gsub(/\\/, '\\\\').gsub(/\*/, '\*')
+ end
+
def to_escaped_for_tsquery_split
scan(/\S+/).map {|x| x.to_escaped_for_tsquery}.join(" & ")
end
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index 1abe96f80..e0463f36e 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -97,6 +97,14 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
end
+
+ should "list aliases before abbreviations" do
+ create(:tag, name: "hair_ribbon", post_count: 300_000)
+ create(:tag, name: "hakurei_reimu", post_count: 50_000)
+ create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
+
+ assert_autocomplete_equals(%w[hakurei_reimu hair_ribbon], "/hr", :tag_query)
+ end
end
should "autocomplete tags from wiki and artist other names" do
diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb
index 193cbbad5..f4882c287 100644
--- a/test/unit/post_query_builder_test.rb
+++ b/test/unit/post_query_builder_test.rb
@@ -1018,6 +1018,17 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post2], "-kitten")
end
+ should "resolve abbreviations to the actual tag" do
+ tag1 = create(:tag, name: "hair_ribbon", post_count: 300_000)
+ tag2 = create(:tag, name: "hakurei_reimu", post_count: 50_000)
+ ta1 = create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
+ post1 = create(:post, tag_string: "hair_ribbon")
+ post2 = create(:post, tag_string: "hakurei_reimu")
+
+ assert_tag_match([post2], "/hr")
+ assert_tag_match([post1], "-/hr")
+ end
+
should "fail for more than 6 tags" do
post1 = create(:post, rating: "s")
diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb
index d08737624..ee470de05 100644
--- a/test/unit/post_test.rb
+++ b/test/unit/post_test.rb
@@ -598,6 +598,10 @@ class PostTest < ActiveSupport::TestCase
context "tagged with a valid tag" do
subject { @post }
+ setup do
+ create(:tag, name: "hakurei_reimu")
+ end
+
should allow_value("touhou 100%").for(:tag_string)
should allow_value("touhou FOO").for(:tag_string)
should allow_value("touhou -foo").for(:tag_string)
@@ -618,6 +622,8 @@ class PostTest < ActiveSupport::TestCase
# \u3000 = ideographic space, \u00A0 = no-break space
should allow_value("touhou\u3000foo").for(:tag_string)
should allow_value("touhou\u00A0foo").for(:tag_string)
+
+ should allow_value("/hr").for(:tag_string)
end
context "tagged with an invalid tag" do
@@ -661,6 +667,16 @@ class PostTest < ActiveSupport::TestCase
end
end
+ context "tagged with an abbreviation" do
+ should "expand the abbreviation" do
+ create(:tag, name: "hair_ribbon", post_count: 300_000)
+ create(:tag, name: "hakurei_reimu", post_count: 50_000)
+
+ @post.update!(tag_string: "aaa /hr")
+ assert_equal("aaa hair_ribbon", @post.reload.tag_string)
+ end
+ end
+
context "tagged with a metatag" do
context "for typing a tag" do
setup do
@@ -1190,6 +1206,17 @@ class PostTest < ActiveSupport::TestCase
assert_equal("aaa", @post.tag_string)
end
+
+ should "resolve abbreviations" do
+ create(:tag, name: "hair_ribbon", post_count: 300_000)
+ create(:tag, name: "hakurei_reimu", post_count: 50_000)
+
+ @post.update!(tag_string: "aaa hair_ribbon hakurei_reimu")
+ assert_equal("aaa hair_ribbon hakurei_reimu", @post.reload.tag_string)
+
+ @post.update!(tag_string: "aaa hair_ribbon hakurei_reimu -/hr")
+ assert_equal("aaa hakurei_reimu", @post.reload.tag_string)
+ end
end
context "tagged with animated_gif or animated_png" do
diff --git a/test/unit/tag_alias_test.rb b/test/unit/tag_alias_test.rb
index 1aeb005b9..4b5a44bc8 100644
--- a/test/unit/tag_alias_test.rb
+++ b/test/unit/tag_alias_test.rb
@@ -79,6 +79,18 @@ class TagAliasTest < ActiveSupport::TestCase
assert_equal(["bbb", "bbb"], TagAlias.to_aliased(["aaa", "aaa"]))
end
+ should "handle abbreviations in TagAlias.to_aliased" do
+ create(:tag, name: "hair_ribbon", post_count: 300_000)
+ create(:tag, name: "hakurei_reimu", post_count: 50_000)
+ create(:tag, name: "kirisama_marisa", post_count: 50_000)
+ create(:tag, name: "kaname_madoka", post_count: 20_000)
+ create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
+
+ assert_equal(["hakurei_reimu"], TagAlias.to_aliased(["/hr"]))
+ assert_equal(["kirisama_marisa"], TagAlias.to_aliased(["/km"]))
+ assert_equal(["hakurei_reimu", "kirisama_marisa"], TagAlias.to_aliased(["/hr", "/km"]))
+ end
+
context "saved searches" do
should "move saved searches" do
@ss1 = create(:saved_search, query: "123 ... 456", user: CurrentUser.user)
From 3d1ff9dff9cde553c781bcffcabeb02497663a4b Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 20:08:33 -0600
Subject: [PATCH 047/132] autocomplete: fix not detecting correct tag in edit
box.
---
app/javascript/src/javascripts/autocomplete.js.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index 4046ccc7d..c3269f358 100644
--- a/app/javascript/src/javascripts/autocomplete.js.erb
+++ b/app/javascript/src/javascripts/autocomplete.js.erb
@@ -110,7 +110,7 @@ Autocomplete.initialize_tag_autocomplete = function() {
Autocomplete.current_term = function($input) {
let query = $input.get(0).value;
let caret = $input.get(0).selectionStart;
- let match = query.substring(0, caret).match(/\S*/);
+ let match = query.substring(0, caret).match(/\S*$/);
return match[0];
};
From 25069865b7560c80e7a7c95881f4d1f21bd857c0 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 20:21:40 -0600
Subject: [PATCH 048/132] ip bans: add search form.
* Add IP ban search form to /ip_bans page.
* Make some attributes searchable that weren't previously searchable.
---
app/models/ip_ban.rb | 12 ++++++++----
app/views/ip_bans/index.html.erb | 10 ++++++++++
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/app/models/ip_ban.rb b/app/models/ip_ban.rb
index 223539f43..d69f41bbf 100644
--- a/app/models/ip_ban.rb
+++ b/app/models/ip_ban.rb
@@ -25,14 +25,18 @@ class IpBan < ApplicationRecord
end
def self.search(params)
- q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator)
+ q = search_attributes(params, :id, :created_at, :updated_at, :ip_addr, :reason, :is_deleted, :category, :hit_count, :last_hit_at, :creator)
q = q.text_attribute_matches(:reason, params[:reason_matches])
- if params[:ip_addr].present?
- q = q.where("ip_addr = ?", params[:ip_addr])
+ case params[:order]
+ when /\A(created_at|updated_at|last_hit_at)(?:_(asc|desc))?\z/i
+ dir = $2 || :desc
+ q = q.order($1 => dir).order(id: :desc)
+ else
+ q = q.apply_default_order(params)
end
- q.apply_default_order(params)
+ q
end
def create_mod_action
diff --git a/app/views/ip_bans/index.html.erb b/app/views/ip_bans/index.html.erb
index 5a68e17f9..b3bdf29db 100644
--- a/app/views/ip_bans/index.html.erb
+++ b/app/views/ip_bans/index.html.erb
@@ -2,6 +2,16 @@
IP Bans
+ <%= search_form_for(ip_bans_path) do |f| %>
+ <%= f.input :ip_addr, label: "IP Addr", hint: "Use /24 for subnet", input_html: { value: params[:search][:ip_addr] } %>
+ <%= f.input :reason, input_html: { value: params[:search][:reason] } %>
+ <%= f.input :creator_name, label: "Creator", input_html: { value: params[:search][:creator_name], "data-autocomplete": "user" } %>
+ <%= f.input :category, collection: IpBan.categories, include_blank: true, selected: params[:search][:category] %>
+ <%= f.input :is_deleted, label: "Deleted?", collection: ["Yes", "No"], include_blank: true, input_html: { value: params[:search][:is_deleted] } %>
+ <%= f.input :order, collection: [%w[Newest created_at], %w[Oldest created_at_asc], %w[Last\ Seen last_hit_at]], include_blank: true, selected: params[:search][:order] %>
+ <%= f.submit "Search" %>
+ <% end %>
+
<%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %>
<% t.column "IP Address" do |ip_ban| %>
<%= link_to_ip ip_ban.subnetted_ip %>
From 2c92794ebaecbf5f4f57ba76d24f0514a53d42b8 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 20:35:38 -0600
Subject: [PATCH 049/132] wiki: include search form on search results page.
Include the search form on the search results page so you can more
easily refine your search.
---
app/views/wiki_pages/_search.html.erb | 9 +++++++++
app/views/wiki_pages/index.html.erb | 2 ++
app/views/wiki_pages/search.html.erb | 12 +-----------
3 files changed, 12 insertions(+), 11 deletions(-)
create mode 100644 app/views/wiki_pages/_search.html.erb
diff --git a/app/views/wiki_pages/_search.html.erb b/app/views/wiki_pages/_search.html.erb
new file mode 100644
index 000000000..cf9bd1834
--- /dev/null
+++ b/app/views/wiki_pages/_search.html.erb
@@ -0,0 +1,9 @@
+<%= search_form_for(wiki_pages_path) do |f| %>
+ <%= f.input :title_normalize, label: "Title", hint: "Use * for wildcard", input_html: { value: params[:search][:title_normalize], "data-autocomplete": "wiki-page" } %>
+ <%= f.input :other_names_match, label: "Other names", hint: "Use * for wildcard", input_html: { value: params[:search][:other_names_match] } %>
+ <%= f.input :body_matches, label: "Body", hint: "Use * for wildcard", input_html: { value: params[:search][:body_matches] } %>
+ <%= f.input :linked_to, hint: "Find wikis linking to this wiki", input_html: { value: params[:search][:linked_to], "data-autocomplete": "wiki-page" } %>
+ <%= f.input :is_deleted, label: "Deleted?", as: :select, include_blank: true, selected: params[:search][:is_deleted] %>
+ <%= f.input :order, collection: [%w[Newest created_at], %w[Title title], %w[Posts post_count]], include_blank: true, selected: params[:search][:order] %>
+ <%= f.submit "Search" %>
+<% end %>
diff --git a/app/views/wiki_pages/index.html.erb b/app/views/wiki_pages/index.html.erb
index 7a03e3d34..c9dc8c4a7 100644
--- a/app/views/wiki_pages/index.html.erb
+++ b/app/views/wiki_pages/index.html.erb
@@ -3,6 +3,8 @@
<% content_for(:content) do %>
Wiki
+ <%= render "search" %>
+
<%= table_for @wiki_pages, width: "100%" do |t| %>
<% t.column "Title" do |wiki_page| %>
<%= link_to_wiki wiki_page.title %>
diff --git a/app/views/wiki_pages/search.html.erb b/app/views/wiki_pages/search.html.erb
index 3297d9695..c21832632 100644
--- a/app/views/wiki_pages/search.html.erb
+++ b/app/views/wiki_pages/search.html.erb
@@ -1,16 +1,6 @@
- <%= search_form_for(wiki_pages_path) do |f| %>
- <%= f.input :title_normalize, label: "Title", hint: "Use * for wildcard searches", input_html: { "data-autocomplete": "wiki-page" } %>
- <%= f.input :other_names_match, label: "Other names", hint: "Use * for wildcard searches" %>
- <%= f.input :body_matches, label: "Body" %>
- <%= f.input :linked_to, hint: "Which wikis link to the specified wiki.", input_html: { "data-autocomplete": "wiki-page" } %>
- <%= f.input :not_linked_to, hint: "Which wikis do not link to the specified wiki.", input_html: { "data-autocomplete": "wiki-page" } %>
- <%= f.input :other_names_present, as: :select %>
- <%= f.input :hide_deleted, as: :select, include_blank: false %>
- <%= f.input :order, collection: [%w[Name title], %w[Date time], %w[Posts post_count]], include_blank: false %>
- <%= f.submit "Search" %>
- <% end %>
+ <%= render "search" %>
From 53653372ec34182b1a5a3f18b6119f2164b21449 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 17 Dec 2020 21:36:43 -0600
Subject: [PATCH 050/132] notes: include search form on search results page.
Also eliminate /notes/search endpoint.
---
app/controllers/notes_controller.rb | 3 ---
app/views/notes/_secondary_links.html.erb | 1 -
app/views/notes/index.html.erb | 10 +++++++++-
app/views/notes/search.html.erb | 15 ---------------
app/views/static/site_map.html.erb | 1 -
config/routes.rb | 3 ---
6 files changed, 9 insertions(+), 24 deletions(-)
delete mode 100644 app/views/notes/search.html.erb
diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb
index ec4246acb..db320a92a 100644
--- a/app/controllers/notes_controller.rb
+++ b/app/controllers/notes_controller.rb
@@ -1,9 +1,6 @@
class NotesController < ApplicationController
respond_to :html, :xml, :json, :js
- def search
- end
-
def index
@notes = authorize Note.paginated_search(params)
@notes = @notes.includes(:post) if request.format.html?
diff --git a/app/views/notes/_secondary_links.html.erb b/app/views/notes/_secondary_links.html.erb
index c3497b7ff..c6b1c2b4c 100644
--- a/app/views/notes/_secondary_links.html.erb
+++ b/app/views/notes/_secondary_links.html.erb
@@ -2,7 +2,6 @@
<%= quick_search_form_for(:body_matches, notes_path, "notes") %>
<%= subnav_link_to "Listing", notes_path %>
<%= subnav_link_to "Posts", posts_path(:tags => "order:note") %>
- <%= subnav_link_to "Search", search_notes_path %>
<%= subnav_link_to "History", note_versions_path %>
<%= subnav_link_to "Requests", posts_path(:tags => "translation_request") %>
<%= subnav_link_to "Help", wiki_page_path("help:notes") %>
diff --git a/app/views/notes/index.html.erb b/app/views/notes/index.html.erb
index c52af8bdb..76b6b9099 100644
--- a/app/views/notes/index.html.erb
+++ b/app/views/notes/index.html.erb
@@ -4,6 +4,14 @@
Notes
+ <%= search_form_for(notes_path) do |f| %>
+ <%= f.hidden_field :group_by, value: "note" %>
+
+ <%= f.input :body_matches, label: "Note", hint: "Use * for wildcard", input_html: { value: params[:search][:body_matches] } %>
+ <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %>
+ <%= f.submit "Search" %>
+ <% end %>
+
<%= table_for @notes, class: "striped autofit" do |t| %>
<% t.column "Post" do |note| %>
<%= link_to note.post_id, note.post %>
@@ -12,7 +20,7 @@
<%= link_to "#{note.id}.#{note.version}", post_path(note.post_id, anchor: "note-#{note.id}") %>
<%= link_to "»", note_versions_path(search: { note_id: note.id }) %>
<% end %>
- <% t.column "Body", td: { class: "col-expand" } do |note| %>
+ <% t.column "Text", td: { class: "col-expand" } do |note| %>
<%= note.body %>
<% unless note.is_active? %>
(deleted)
diff --git a/app/views/notes/search.html.erb b/app/views/notes/search.html.erb
deleted file mode 100644
index 4aabd7e71..000000000
--- a/app/views/notes/search.html.erb
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
Search Notes
-
- <%= search_form_for(notes_path) do |f| %>
- <%= f.hidden_field :group_by, value: "note" %>
-
- <%= f.input :body_matches, label: "Body" %>
- <%= f.input :post_tags_match, label: "Tags", input_html: { data: { autocomplete: "tag-query" } } %>
- <%= f.submit "Search" %>
- <% end %>
-
-
-
-<%= render "secondary_links" %>
diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index 4e82675d2..893ca7b3b 100644
--- a/app/views/static/site_map.html.erb
+++ b/app/views/static/site_map.html.erb
@@ -57,7 +57,6 @@
Notes
<%= link_to_wiki "Help", "help:notes" %>
<%= link_to("Listing", notes_path) %>
-
<%= link_to("Search", search_notes_path) %>
<%= link_to("Changes", note_versions_path) %>
diff --git a/config/routes.rb b/config/routes.rb
index 9b916feaf..6244fed13 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -142,9 +142,6 @@ Rails.application.routes.draw do
resources :modqueue, only: [:index]
resources :news_updates
resources :notes do
- collection do
- get :search
- end
member do
put :revert
end
From 5fc99b9946808a592c2737297ff948f0995e9d43 Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 18 Dec 2020 02:04:43 -0600
Subject: [PATCH 051/132] Upgrade to Rails 6.1.
* Swap out activerecord-hierarchical_query gem for some guy's patched
version because the mainline version is incompatible with 6.1.
* Disable meta_request gem because it hangs puma on startup on 6.1.
---
Gemfile | 4 +-
Gemfile.lock | 151 ++++++++++++++++++++++++++-------------------------
2 files changed, 79 insertions(+), 76 deletions(-)
diff --git a/Gemfile b/Gemfile
index 92332c2e6..eed0df543 100644
--- a/Gemfile
+++ b/Gemfile
@@ -40,7 +40,7 @@ gem 'puma'
gem 'scenic'
gem 'ipaddress_2'
gem 'http'
-gem 'activerecord-hierarchical_query'
+gem 'activerecord-hierarchical_query', git: "https://github.com/walski/activerecord-hierarchical_query", branch: "rails-6-1"
gem 'http-cookie', git: "https://github.com/danbooru/http-cookie"
gem 'pundit'
gem 'mail'
@@ -60,7 +60,7 @@ end
group :development do
gem 'rubocop'
gem 'rubocop-rails'
- gem 'meta_request'
+ # gem 'meta_request' # hangs on Rails 6.1
gem 'rack-mini-profiler'
gem 'stackprof'
gem 'flamegraph'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2fb7f8a00..021635222 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -12,71 +12,81 @@ GIT
dtext_rb (1.10.6)
nokogiri (~> 1.8)
+GIT
+ remote: https://github.com/walski/activerecord-hierarchical_query
+ revision: 3d6663307ed2f6a23347084c04700a26c7e7bb55
+ branch: rails-6-1
+ specs:
+ activerecord-hierarchical_query (1.2.3)
+ activerecord (>= 5.0, < 6.2)
+ pg (>= 0.21, < 1.3)
+
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.0.3.4)
- actionpack (= 6.0.3.4)
+ actioncable (6.1.0)
+ actionpack (= 6.1.0)
+ activesupport (= 6.1.0)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.0.3.4)
- actionpack (= 6.0.3.4)
- activejob (= 6.0.3.4)
- activerecord (= 6.0.3.4)
- activestorage (= 6.0.3.4)
- activesupport (= 6.0.3.4)
+ actionmailbox (6.1.0)
+ actionpack (= 6.1.0)
+ activejob (= 6.1.0)
+ activerecord (= 6.1.0)
+ activestorage (= 6.1.0)
+ activesupport (= 6.1.0)
mail (>= 2.7.1)
- actionmailer (6.0.3.4)
- actionpack (= 6.0.3.4)
- actionview (= 6.0.3.4)
- activejob (= 6.0.3.4)
+ actionmailer (6.1.0)
+ actionpack (= 6.1.0)
+ actionview (= 6.1.0)
+ activejob (= 6.1.0)
+ activesupport (= 6.1.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.0.3.4)
- actionview (= 6.0.3.4)
- activesupport (= 6.0.3.4)
- rack (~> 2.0, >= 2.0.8)
+ actionpack (6.1.0)
+ actionview (= 6.1.0)
+ activesupport (= 6.1.0)
+ rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.0.3.4)
- actionpack (= 6.0.3.4)
- activerecord (= 6.0.3.4)
- activestorage (= 6.0.3.4)
- activesupport (= 6.0.3.4)
+ actiontext (6.1.0)
+ actionpack (= 6.1.0)
+ activerecord (= 6.1.0)
+ activestorage (= 6.1.0)
+ activesupport (= 6.1.0)
nokogiri (>= 1.8.5)
- actionview (6.0.3.4)
- activesupport (= 6.0.3.4)
+ actionview (6.1.0)
+ activesupport (= 6.1.0)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.0.3.4)
- activesupport (= 6.0.3.4)
+ activejob (6.1.0)
+ activesupport (= 6.1.0)
globalid (>= 0.3.6)
- activemodel (6.0.3.4)
- activesupport (= 6.0.3.4)
+ activemodel (6.1.0)
+ activesupport (= 6.1.0)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
- activerecord (6.0.3.4)
- activemodel (= 6.0.3.4)
- activesupport (= 6.0.3.4)
- activerecord-hierarchical_query (1.2.3)
- activerecord (>= 5.0, < 6.1)
- pg (>= 0.21, < 1.3)
- activestorage (6.0.3.4)
- actionpack (= 6.0.3.4)
- activejob (= 6.0.3.4)
- activerecord (= 6.0.3.4)
+ activerecord (6.1.0)
+ activemodel (= 6.1.0)
+ activesupport (= 6.1.0)
+ activestorage (6.1.0)
+ actionpack (= 6.1.0)
+ activejob (= 6.1.0)
+ activerecord (= 6.1.0)
+ activesupport (= 6.1.0)
marcel (~> 0.3.1)
- activesupport (6.0.3.4)
+ mimemagic (~> 0.3.2)
+ activesupport (6.1.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 0.7, < 2)
- minitest (~> 5.1)
- tzinfo (~> 1.1)
- zeitwerk (~> 2.2, >= 2.2.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ zeitwerk (~> 2.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.4.0)
@@ -84,13 +94,13 @@ GEM
ansi (1.5.0)
ast (2.4.1)
aws-eventstream (1.1.0)
- aws-partitions (1.405.0)
+ aws-partitions (1.409.0)
aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-sqs (1.34.0)
+ aws-sdk-sqs (1.35.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
@@ -176,7 +186,7 @@ GEM
concurrent-ruby (~> 1.0)
ipaddress_2 (0.13.0)
jmespath (1.4.0)
- json (2.3.1)
+ json (2.4.1)
jwt (2.2.2)
kgio (2.11.3)
listen (3.3.3)
@@ -191,9 +201,6 @@ GEM
mimemagic (~> 0.3.2)
memoist (0.16.2)
memory_profiler (1.0.0)
- meta_request (0.7.2)
- rack-contrib (>= 1.1, < 3)
- railties (>= 3.0.0, < 7)
method_source (1.0.0)
mimemagic (0.3.5)
mini_mime (1.0.2)
@@ -247,40 +254,38 @@ GEM
pundit (2.1.0)
activesupport (>= 3.0.0)
rack (2.2.3)
- rack-contrib (2.3.0)
- rack (~> 2.0)
rack-mini-profiler (2.2.0)
rack (>= 1.2.0)
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.0.3.4)
- actioncable (= 6.0.3.4)
- actionmailbox (= 6.0.3.4)
- actionmailer (= 6.0.3.4)
- actionpack (= 6.0.3.4)
- actiontext (= 6.0.3.4)
- actionview (= 6.0.3.4)
- activejob (= 6.0.3.4)
- activemodel (= 6.0.3.4)
- activerecord (= 6.0.3.4)
- activestorage (= 6.0.3.4)
- activesupport (= 6.0.3.4)
- bundler (>= 1.3.0)
- railties (= 6.0.3.4)
+ rails (6.1.0)
+ actioncable (= 6.1.0)
+ actionmailbox (= 6.1.0)
+ actionmailer (= 6.1.0)
+ actionpack (= 6.1.0)
+ actiontext (= 6.1.0)
+ actionview (= 6.1.0)
+ activejob (= 6.1.0)
+ activemodel (= 6.1.0)
+ activerecord (= 6.1.0)
+ activestorage (= 6.1.0)
+ activesupport (= 6.1.0)
+ bundler (>= 1.15.0)
+ railties (= 6.1.0)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
- railties (6.0.3.4)
- actionpack (= 6.0.3.4)
- activesupport (= 6.0.3.4)
+ railties (6.1.0)
+ actionpack (= 6.1.0)
+ activesupport (= 6.1.0)
method_source
rake (>= 0.8.7)
- thor (>= 0.20.3, < 2.0)
+ thor (~> 1.0)
rainbow (3.0.0)
raindrops (0.19.1)
rake (13.0.1)
@@ -309,7 +314,7 @@ GEM
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (1.3.0)
parser (>= 2.7.1.5)
- rubocop-rails (2.9.0)
+ rubocop-rails (2.9.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 0.90.0, < 2.0)
@@ -360,9 +365,8 @@ GEM
multi_json (~> 1.0)
stripe (> 5, < 6)
thor (1.0.1)
- thread_safe (0.3.6)
- tzinfo (1.2.8)
- thread_safe (~> 0.1)
+ tzinfo (2.0.4)
+ concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
@@ -392,7 +396,7 @@ PLATFORMS
DEPENDENCIES
activemodel-serializers-xml
- activerecord-hierarchical_query
+ activerecord-hierarchical_query!
addressable
aws-sdk-sqs (~> 1)
bcrypt
@@ -421,7 +425,6 @@ DEPENDENCIES
mail
memoist
memory_profiler
- meta_request
minitest-ci
minitest-reporters
mocha
From 6849a3d68b927aac64a5bd8d0b50cd93fbe63114 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 00:26:27 -0600
Subject: [PATCH 052/132] Update app files to Rails 6.1 defaults.
---
bin/rails | 4 +-
bin/rake | 31 +--------
bin/setup | 8 +--
bin/yarn | 14 ++++-
config.ru | 3 +-
config/boot.rb | 4 +-
config/environment.rb | 2 +-
config/environments/development.rb | 30 +++++----
config/environments/production.rb | 34 +++++-----
config/environments/test.rb | 16 +++--
config/initializers/backtrace_silencers.rb | 7 ++-
.../new_framework_defaults_6_1.rb | 63 +++++++++++++++++++
config/initializers/permissions_policy.rb | 11 ++++
config/puma.rb | 41 +++++-------
14 files changed, 160 insertions(+), 108 deletions(-)
create mode 100644 config/initializers/new_framework_defaults_6_1.rb
create mode 100644 config/initializers/permissions_policy.rb
diff --git a/bin/rails b/bin/rails
index 073966023..6fb4e4051 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,4 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
-require_relative '../config/boot'
-require 'rails/commands'
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/bin/rake b/bin/rake
index 9275675e8..4fbf10b96 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,29 +1,4 @@
#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-#
-# This file was generated by Bundler.
-#
-# The application 'rake' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
-require "pathname"
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
- Pathname.new(__FILE__).realpath)
-
-bundle_binstub = File.expand_path("../bundle", __FILE__)
-
-if File.file?(bundle_binstub)
- if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
- load(bundle_binstub)
- else
- abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
-Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
- end
-end
-
-require "rubygems"
-require "bundler/setup"
-
-load Gem.bin_path("rake", "rake")
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/bin/setup b/bin/setup
index 5853b5ea8..90700ac4f 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-require 'fileutils'
+require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
@@ -9,8 +9,8 @@ def system!(*args)
end
FileUtils.chdir APP_ROOT do
- # This script is a way to setup or update your development environment automatically.
- # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
+ # This script is a way to set up or update your development environment automatically.
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
@@ -18,7 +18,7 @@ FileUtils.chdir APP_ROOT do
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies
- # system('bin/yarn')
+ system! 'bin/yarn'
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
diff --git a/bin/yarn b/bin/yarn
index 460dd565b..241546e51 100755
--- a/bin/yarn
+++ b/bin/yarn
@@ -1,9 +1,17 @@
#!/usr/bin/env ruby
+require 'pathname'
+
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
- begin
- exec "yarnpkg", *ARGV
- rescue Errno::ENOENT
+ executable_path = ENV["PATH"].split(File::PATH_SEPARATOR).find do |path|
+ normalized_path = File.expand_path(path)
+
+ normalized_path != __dir__ && File.executable?(Pathname.new(normalized_path).join('yarn'))
+ end
+
+ if executable_path
+ exec File.expand_path(Pathname.new(executable_path).join('yarn')), *ARGV
+ else
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
diff --git a/config.ru b/config.ru
index 401c7482f..a0a19dff8 100644
--- a/config.ru
+++ b/config.ru
@@ -1,6 +1,6 @@
# This file is used by Rack-based servers to start the application.
-require ::File.expand_path('../config/environment', __FILE__)
+require_relative "config/environment"
if defined?(Unicorn) && Rails.env.production?
# Unicorn self-process killer
@@ -14,3 +14,4 @@ if defined?(Unicorn) && Rails.env.production?
end
run Rails.application
+Rails.application.load_server
diff --git a/config/boot.rb b/config/boot.rb
index b9e460cef..3cda23b4d 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,4 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
-require 'bundler/setup' # Set up gems listed in the Gemfile.
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+require "bundler/setup" # Set up gems listed in the Gemfile.
+require "bootsnap/setup" # Speed up boot time by caching expensive operations.
diff --git a/config/environment.rb b/config/environment.rb
index 426333bb4..cac531577 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,5 @@
# Load the Rails application.
-require_relative 'application'
+require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 9b5b48df6..f0d62dc0c 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,8 +1,10 @@
+require "active_support/core_ext/integer/time"
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
- # In the development environment your application's code is reloaded on
- # every request. This slows down response time but is perfect for development
+ # In the development environment your application's code is reloaded any time
+ # it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
@@ -28,9 +30,6 @@ Rails.application.configure do
config.cache_store = :null_store
end
- # Store uploaded files on the local file system (see config/storage.yml for options).
- # config.active_storage.service = :local
-
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
@@ -39,24 +38,29 @@ Rails.application.configure do
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
- # Debug mode disables concatenation and preprocessing of assets.
- # This option may cause significant delays in view rendering with a large
- # number of complex assets.
- # config.assets.debug = true
-
- # Suppress logger output for asset requests.
- # config.assets.quiet = true
# Raises error for missing translations.
- # config.action_view.raise_on_missing_translations = true
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+ # Uncomment if you wish to allow Action Cable access from any origin.
+ # config.action_cable.disable_request_forgery_protection = true
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 4550af040..21706aae9 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,3 +1,5 @@
+require "active_support/core_ext/integer/time"
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -22,36 +24,22 @@ Rails.application.configure do
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
- # Compress CSS using a preprocessor.
- # config.assets.css_compressor = :sass
-
- # Do not fallback to assets pipeline if a precompiled asset is missed.
- # config.assets.compile = false
-
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
- # config.action_controller.asset_host = 'http://assets.example.com'
+ # config.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
- # Store uploaded files on the local file system (see config/storage.yml for options).
- # config.active_storage.service = :local
-
- # Mount Action Cable outside main process or domain.
- # config.action_cable.mount_path = nil
- # config.action_cable.url = 'wss://example.com/cable'
- # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
-
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
- # Use the lowest log level to ensure availability of diagnostic information
- # when problems arise.
+ # Include generic and useful information about system operation, but avoid logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII).
config.log_level = :error
# Prepend all log lines with the following tags.
- config.log_tags = [:request_id]
+ config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
@@ -70,16 +58,22 @@ Rails.application.configure do
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
- config.i18n.fallbacks = [I18n.default_locale]
+ config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
+ # Log disallowed deprecations.
+ config.active_support.disallowed_deprecation = :log
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
- # require 'syslog/logger'
+ # require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
diff --git a/config/environments/test.rb b/config/environments/test.rb
index eebb71d5e..d66c042f9 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,3 +1,5 @@
+require "active_support/core_ext/integer/time"
+
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
@@ -30,9 +32,6 @@ Rails.application.configure do
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
- # Store uploaded files on the local file system in a temporary directory.
- # config.active_storage.service = :test
-
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
@@ -43,6 +42,15 @@ Rails.application.configure do
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
# Raises error for missing translations.
- # config.action_view.raise_on_missing_translations = true
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
end
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
index 59385cdf3..33699c309 100644
--- a/config/initializers/backtrace_silencers.rb
+++ b/config/initializers/backtrace_silencers.rb
@@ -1,7 +1,8 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
+# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
+Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
diff --git a/config/initializers/new_framework_defaults_6_1.rb b/config/initializers/new_framework_defaults_6_1.rb
new file mode 100644
index 000000000..629888deb
--- /dev/null
+++ b/config/initializers/new_framework_defaults_6_1.rb
@@ -0,0 +1,63 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.1 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Support for inversing belongs_to -> has_many Active Record associations.
+# Rails.application.config.active_record.has_many_inversing = true
+
+# Track Active Storage variants in the database.
+# Rails.application.config.active_storage.track_variants = true
+
+# Apply random variation to the delay when retrying failed jobs.
+# Rails.application.config.active_job.retry_jitter = 0.15
+
+# Stop executing `after_enqueue`/`after_perform` callbacks if
+# `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
+# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true
+
+# Specify cookies SameSite protection level: either :none, :lax, or :strict.
+#
+# This change is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.1.
+# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
+
+# Generate CSRF tokens that are encoded in URL-safe Base64.
+#
+# This change is not backwards compatible with earlier Rails versions.
+# It's best enabled when your entire app is migrated and stable on 6.1.
+# Rails.application.config.action_controller.urlsafe_csrf_tokens = true
+
+# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
+# UTC offset or a UTC time.
+# ActiveSupport.utc_to_local_returns_utc_offset_times = true
+
+# Change the default HTTP status code to `308` when redirecting non-GET/HEAD
+# requests to HTTPS in `ActionDispatch::SSL` middleware.
+# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308
+
+# Use new connection handling API. For most applications this won't have any
+# effect. For applications using multiple databases, this new API provides
+# support for granular connection swapping.
+# Rails.application.config.active_record.legacy_connection_handling = false
+
+# Make `form_with` generate non-remote forms by default.
+# Rails.application.config.action_view.form_with_generates_remote_forms = false
+
+# Set the default queue name for the analysis job to the queue adapter default.
+# Rails.application.config.active_storage.queues.analysis = nil
+
+# Set the default queue name for the purge job to the queue adapter default.
+# Rails.application.config.active_storage.queues.purge = nil
+
+# Set the default queue name for the incineration job to the queue adapter default.
+# Rails.application.config.action_mailbox.queues.incineration = nil
+
+# Set the default queue name for the routing job to the queue adapter default.
+# Rails.application.config.action_mailbox.queues.routing = nil
+
+# Set the default queue name for the mail deliver job to the queue adapter default.
+# Rails.application.config.action_mailer.deliver_later_queue_name = nil
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
new file mode 100644
index 000000000..00f64d71b
--- /dev/null
+++ b/config/initializers/permissions_policy.rb
@@ -0,0 +1,11 @@
+# Define an application-wide HTTP permissions policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.permissions_policy do |f|
+# f.camera :none
+# f.gyroscope :none
+# f.microphone :none
+# f.usb :none
+# f.fullscreen :self
+# f.payment :self, "https://secure.example.com"
+# end
diff --git a/config/puma.rb b/config/puma.rb
index 1e19380dc..d9b3e836c 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -4,19 +4,28 @@
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
-threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
-threads threads_count, threads_count
+max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `worker_timeout` threshold that Puma will use to wait before
+# terminating a worker in development environments.
+#
+worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+
# Specifies the number of `workers` to boot in clustered mode.
-# Workers are forked webserver processes. If using threads and workers together
+# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
@@ -26,31 +35,9 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
-# process behavior so workers use less memory. If you use this option
-# you need to make sure to reconnect any threads in the `on_worker_boot`
-# block.
+# process behavior so workers use less memory.
#
# preload_app!
-# If you are preloading your application and using Active Record, it's
-# recommended that you close any connections to the database before workers
-# are forked to prevent connection leakage.
-#
-# before_fork do
-# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
-# end
-
-# The code in the `on_worker_boot` will be called if you are using
-# clustered mode by specifying a number of `workers`. After each worker
-# process is booted, this block will be run. If you are using the `preload_app!`
-# option, you will want to use this block to reconnect to any threads
-# or connections that may have been created at application boot, as Ruby
-# cannot share connections between processes.
-#
-# on_worker_boot do
-# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
-# end
-#
-
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
From c97186abd7889b942b6acbdc5b6a5442b6d8f74f Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 00:27:20 -0600
Subject: [PATCH 053/132] /status: add missing template files.
Add missing templates that were forgotten in 7a87225ac.
---
app/views/status/_list.html.erb | 7 +++++++
app/views/status/_table.html.erb | 19 +++++++++++++++++++
2 files changed, 26 insertions(+)
create mode 100644 app/views/status/_list.html.erb
create mode 100644 app/views/status/_table.html.erb
diff --git a/app/views/status/_list.html.erb b/app/views/status/_list.html.erb
new file mode 100644
index 000000000..e5b304b21
--- /dev/null
+++ b/app/views/status/_list.html.erb
@@ -0,0 +1,7 @@
+<%# hash %>
+
+ <% hash.each do |key, value| %>
+ <%= key.to_s.humanize %>
+ <%= value %>
+ <% end %>
+
diff --git a/app/views/status/_table.html.erb b/app/views/status/_table.html.erb
new file mode 100644
index 000000000..05678ff5c
--- /dev/null
+++ b/app/views/status/_table.html.erb
@@ -0,0 +1,19 @@
+<%# rows %>
+
+
+
+
+ <% rows.first.keys.each do |key| %>
+ <%= key.humanize %>
+ <% end %>
+
+
+
+ <% rows.each do |row| %>
+
+ <% row.each do |key, value| %>
+ <%= value %>
+ <% end %>
+
+ <% end %>
+
From 09e3146819958df64b065248c19342ab4161ed08 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 00:51:34 -0600
Subject: [PATCH 054/132] artist finder: add blog.livedoor.jp to blacklist.
---
app/logical/artist_finder.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/logical/artist_finder.rb b/app/logical/artist_finder.rb
index a0e8f21b9..bd24b8645 100644
--- a/app/logical/artist_finder.rb
+++ b/app/logical/artist_finder.rb
@@ -60,6 +60,7 @@ module ArtistFinder
"iwara.tv/users", # http://ecchi.iwara.tv/users/marumega
"kym-cdn.com",
"livedoor.blogimg.jp",
+ "blog.livedoor.jp", # http://blog.livedoor.jp/ac370ml
"monappy.jp",
"monappy.jp/u", # https://monappy.jp/u/abara_bone
"mstdn.jp", # https://mstdn.jp/@oneb
From 4cb39422b21c04e22a88acf4b8c20f1a31244795 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 14:18:05 -0600
Subject: [PATCH 055/132] post replacements: rename _was to old_
Rename the following post replacement attributes:
* file_size_was -> old_file_size
* file_ext_was -> old_file_ext
* image_width_was -> old_image_width
* image_height_was -> old_image_height
* md5_was -> old_md5
In Rails 6.1, having attributes named `file_size` and `file_size_was` on
the same model breaks things because it conflicts with Rails' dirty
attribute tracking.
---
app/models/post_replacement.rb | 12 ++++++------
app/policies/post_replacement_policy.rb | 4 ++--
app/views/post_replacements/index.html.erb | 10 +++++-----
...1219201007_rename_post_replacement_attributes.rb | 9 +++++++++
db/structure.sql | 13 +++++++------
.../functional/post_replacements_controller_test.rb | 4 ++--
test/unit/upload_service_test.rb | 10 +++++-----
7 files changed, 36 insertions(+), 26 deletions(-)
create mode 100644 db/migrate/20201219201007_rename_post_replacement_attributes.rb
diff --git a/app/models/post_replacement.rb b/app/models/post_replacement.rb
index ae5a23174..290e54984 100644
--- a/app/models/post_replacement.rb
+++ b/app/models/post_replacement.rb
@@ -11,17 +11,17 @@ class PostReplacement < ApplicationRecord
self.original_url = post.source
self.tags = post.tag_string + " " + self.tags.to_s
- self.file_ext_was = post.file_ext
- self.file_size_was = post.file_size
- self.image_width_was = post.image_width
- self.image_height_was = post.image_height
- self.md5_was = post.md5
+ self.old_file_ext = post.file_ext
+ self.old_file_size = post.file_size
+ self.old_image_width = post.image_width
+ self.old_image_height = post.image_height
+ self.old_md5 = post.md5
end
concerning :Search do
class_methods do
def search(params = {})
- q = search_attributes(params, :id, :created_at, :updated_at, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url, :creator, :post)
+ q = search_attributes(params, :id, :created_at, :updated_at, :md5, :old_md5, :file_ext, :old_file_ext, :original_url, :replacement_url, :creator, :post)
q.apply_default_order(params)
end
end
diff --git a/app/policies/post_replacement_policy.rb b/app/policies/post_replacement_policy.rb
index 0cec75193..2a03d6bee 100644
--- a/app/policies/post_replacement_policy.rb
+++ b/app/policies/post_replacement_policy.rb
@@ -12,8 +12,8 @@ class PostReplacementPolicy < ApplicationPolicy
end
def permitted_attributes_for_update
- [:file_ext_was, :file_size_was, :image_width_was, :image_height_was,
- :md5_was, :file_ext, :file_size, :image_width, :image_height, :md5,
+ [:old_file_ext, :old_file_size, :old_image_width, :old_image_height,
+ :old_md5, :file_ext, :file_size, :image_width, :image_height, :md5,
:original_url, :replacement_url]
end
end
diff --git a/app/views/post_replacements/index.html.erb b/app/views/post_replacements/index.html.erb
index b0dd09c87..59a766341 100644
--- a/app/views/post_replacements/index.html.erb
+++ b/app/views/post_replacements/index.html.erb
@@ -29,10 +29,10 @@
<% end %>
<% t.column "MD5" do |post_replacement| %>
- <% if post_replacement.md5_was.present? && post_replacement.md5.present? %>
+ <% if post_replacement.old_md5.present? && post_replacement.md5.present? %>
Original MD5
- <%= post_replacement.md5_was %>
+ <%= post_replacement.old_md5 %>
Replacement MD5
<%= post_replacement.md5 %>
@@ -40,12 +40,12 @@
<% end %>
<% end %>
<% t.column "Size" do |post_replacement| %>
- <% if %i[image_width_was image_height_was file_size_was file_ext_was image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
+ <% if %i[old_image_width old_image_height old_file_size old_file_ext image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
Original Size
- <%= post_replacement.image_width_was %>x<%= post_replacement.image_height_was %>
- (<%= post_replacement.file_size_was.to_s(:human_size, precision: 4) %>, <%= post_replacement.file_ext_was %>)
+ <%= post_replacement.old_image_width %>x<%= post_replacement.old_image_height %>
+ (<%= post_replacement.old_file_size.to_s(:human_size, precision: 4) %>, <%= post_replacement.old_file_ext %>)
Replacement Size
diff --git a/db/migrate/20201219201007_rename_post_replacement_attributes.rb b/db/migrate/20201219201007_rename_post_replacement_attributes.rb
new file mode 100644
index 000000000..31584c920
--- /dev/null
+++ b/db/migrate/20201219201007_rename_post_replacement_attributes.rb
@@ -0,0 +1,9 @@
+class RenamePostReplacementAttributes < ActiveRecord::Migration[6.1]
+ def change
+ rename_column :post_replacements, :file_ext_was, :old_file_ext
+ rename_column :post_replacements, :file_size_was, :old_file_size
+ rename_column :post_replacements, :image_width_was, :old_image_width
+ rename_column :post_replacements, :image_height_was, :old_image_height
+ rename_column :post_replacements, :md5_was, :old_md5
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index d85f9a64c..5f8a9b7da 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -2736,11 +2736,11 @@ CREATE TABLE public.post_replacements (
replacement_url text NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
- file_ext_was character varying,
- file_size_was integer,
- image_width_was integer,
- image_height_was integer,
- md5_was character varying,
+ old_file_ext character varying,
+ old_file_size integer,
+ old_image_width integer,
+ old_image_height integer,
+ old_md5 character varying,
file_ext character varying,
file_size integer,
image_width integer,
@@ -7436,6 +7436,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200803022359'),
('20200816175151'),
('20201201211748'),
-('20201213052805');
+('20201213052805'),
+('20201219201007');
diff --git a/test/functional/post_replacements_controller_test.rb b/test/functional/post_replacements_controller_test.rb
index e9021d1de..21525bcc1 100644
--- a/test/functional/post_replacements_controller_test.rb
+++ b/test/functional/post_replacements_controller_test.rb
@@ -47,14 +47,14 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
format: :json,
id: @post_replacement.id,
post_replacement: {
- file_size_was: 23,
+ old_file_size: 23,
file_size: 42
}
}
put_auth post_replacement_path(@post_replacement), @mod, params: params
assert_response :success
- assert_equal(23, @post_replacement.reload.file_size_was)
+ assert_equal(23, @post_replacement.reload.old_file_size)
assert_equal(42, @post_replacement.file_size)
end
end
diff --git a/test/unit/upload_service_test.rb b/test/unit/upload_service_test.rb
index c8041ebfe..2828b30ab 100644
--- a/test/unit/upload_service_test.rb
+++ b/test/unit/upload_service_test.rb
@@ -287,11 +287,11 @@ class UploadServiceTest < ActiveSupport::TestCase
should "preserve the old values" do
as(@user) { subject.process! }
- assert_equal(1500, @replacement.image_width_was)
- assert_equal(1000, @replacement.image_height_was)
- assert_equal(2000, @replacement.file_size_was)
- assert_equal("jpg", @replacement.file_ext_was)
- assert_equal(@old_md5, @replacement.md5_was)
+ assert_equal(1500, @replacement.old_image_width)
+ assert_equal(1000, @replacement.old_image_height)
+ assert_equal(2000, @replacement.old_file_size)
+ assert_equal("jpg", @replacement.old_file_ext)
+ assert_equal(@old_md5, @replacement.old_md5)
end
should "record the new values" do
From 7708e2e08f9c3fe8a254ebb5a0e319b72fd21313 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 14:42:49 -0600
Subject: [PATCH 056/132] wikis: don't allow adding other names to artist
wikis.
Prevent users from adding other names to artist wikis. These should be
added to the artist entry instead.
---
app/models/wiki_page.rb | 8 ++++++++
app/views/wiki_pages/_form.html.erb | 5 ++++-
test/unit/wiki_page_test.rb | 10 ++++++++++
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 3b0b28045..a60e10863 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -7,10 +7,12 @@ class WikiPage < ApplicationRecord
before_save :normalize_other_names
before_save :update_dtext_links, if: :dtext_links_changed?
after_save :create_version
+
validates_uniqueness_of :title, :case_sensitive => false
validates_presence_of :title
validates_presence_of :body, :unless => -> { is_deleted? || other_names.present? }
validate :validate_rename
+ validate :validate_other_names
array_attribute :other_names
has_one :tag, :foreign_key => "name", :primary_key => "title"
@@ -124,6 +126,12 @@ class WikiPage < ApplicationRecord
end
end
+ def validate_other_names
+ if other_names.present? && tag&.artist?
+ errors.add(:base, "An artist wiki can't have other names")
+ end
+ end
+
def revert_to(version)
if id != version.wiki_page_id
raise RevertError.new("You cannot revert to a previous version of another wiki page.")
diff --git a/app/views/wiki_pages/_form.html.erb b/app/views/wiki_pages/_form.html.erb
index 8f5616dc7..2ff231fd8 100644
--- a/app/views/wiki_pages/_form.html.erb
+++ b/app/views/wiki_pages/_form.html.erb
@@ -3,7 +3,10 @@
<%= edit_form_for(@wiki_page, url: wiki_page_path(@wiki_page.id)) do |f| %>
<%= f.input :title, error: false, input_html: { data: { autocomplete: "tag" } }, hint: "Change to rename this wiki page. Update any wikis linking to this page first." %>
- <%= f.input :other_names_string, as: :text, input_html: { size: "30x1" }, label: "Other names (#{link_to_wiki "help", "help:translated_tags"})".html_safe, hint: "Names used for this tag on other sites such as Pixiv. Separate with spaces." %>
+
+ <% if !@wiki_page.tag&.artist? || @wiki_page.other_names.present? %>
+ <%= f.input :other_names_string, as: :text, input_html: { size: "30x1" }, label: "Other names (#{link_to_wiki "help", "help:translated_tags"})".html_safe, hint: "Names used for this tag on other sites such as Pixiv. Separate with spaces." %>
+ <% end %>
<%= f.input :body, as: :dtext %>
diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb
index bd52fc719..41b814a8a 100644
--- a/test/unit/wiki_page_test.rb
+++ b/test/unit/wiki_page_test.rb
@@ -77,5 +77,15 @@ class WikiPageTest < ActiveSupport::TestCase
assert_equal(0, @wiki_page.dtext_links.size)
end
end
+
+ context "with other names" do
+ should "not allow artist wikis to have other names" do
+ tag = create(:artist_tag)
+ wiki = build(:wiki_page, title: tag.name, other_names: ["blah"])
+
+ assert_equal(false, wiki.valid?)
+ assert_equal(["An artist wiki can't have other names"], wiki.errors[:base])
+ end
+ end
end
end
From a129eb4251f5a49e123c632467e081b81980c5ff Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 17:51:11 -0600
Subject: [PATCH 057/132] wikis: force wiki names to follow same rules as tag
names.
Don't allow wiki pages to have invalid names.
This incidentally means that you can't create wiki pages for pools. For
example, you can't create a wiki titled "pool:almost_heart-warming".
This is not a valid tag name, so it's not a valid wiki name either. This
was done in a handful of cases to translate Pixiv tags to Danbooru pools
(see: )
Also fix it so that titles are normalized before validation, not before save.
---
app/models/wiki_page.rb | 12 ++++------
test/functional/wiki_pages_controller_test.rb | 7 ------
test/unit/wiki_page_test.rb | 23 +++++++++++++++++++
3 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index a60e10863..79922b98c 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -3,14 +3,13 @@ class WikiPage < ApplicationRecord
META_WIKIS = ["list_of_", "tag_group:", "pool_group:", "howto:", "about:", "help:", "template:"]
- before_save :normalize_title
- before_save :normalize_other_names
+ before_validation :normalize_title
+ before_validation :normalize_other_names
before_save :update_dtext_links, if: :dtext_links_changed?
after_save :create_version
- validates_uniqueness_of :title, :case_sensitive => false
- validates_presence_of :title
- validates_presence_of :body, :unless => -> { is_deleted? || other_names.present? }
+ validates :title, tag_name: true, presence: true, uniqueness: true, if: :title_changed?
+ validates :body, presence: true, unless: -> { is_deleted? || other_names.present? }
validate :validate_rename
validate :validate_other_names
@@ -149,8 +148,7 @@ class WikiPage < ApplicationRecord
end
def self.normalize_title(title)
- return if title.blank?
- title.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
+ title.to_s.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
end
def normalize_title
diff --git a/test/functional/wiki_pages_controller_test.rb b/test/functional/wiki_pages_controller_test.rb
index 5a4ad9c2b..0c8ae20b3 100644
--- a/test/functional/wiki_pages_controller_test.rb
+++ b/test/functional/wiki_pages_controller_test.rb
@@ -109,13 +109,6 @@ class WikiPagesControllerTest < ActionDispatch::IntegrationTest
assert_response 404
end
- should "render for a negated tag" do
- as(@user) { @wiki_page.update(title: "-aaa") }
-
- get wiki_page_path(@wiki_page.id)
- assert_redirected_to wiki_page_path(@wiki_page.title)
- end
-
should "work for a title containing dots" do
as(@user) { create(:wiki_page, title: "...") }
diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb
index 41b814a8a..32391cfb8 100644
--- a/test/unit/wiki_page_test.rb
+++ b/test/unit/wiki_page_test.rb
@@ -78,6 +78,29 @@ class WikiPageTest < ActiveSupport::TestCase
end
end
+ context "during title validation" do
+ # these values are allowed because they're normalized first
+ should allow_value(" foo ").for(:title).on(:create)
+ should allow_value("~foo").for(:title).on(:create)
+ should allow_value("_foo").for(:title).on(:create)
+ should allow_value("foo_").for(:title).on(:create)
+ should allow_value("foo__bar").for(:title).on(:create)
+ should allow_value("FOO").for(:title).on(:create)
+ should allow_value("foo bar").for(:title).on(:create)
+
+ should_not allow_value("").for(:title).on(:create)
+ should_not allow_value("___").for(:title).on(:create)
+ should_not allow_value("-foo").for(:title).on(:create)
+ should_not allow_value("/foo").for(:title).on(:create)
+ should_not allow_value("foo*bar").for(:title).on(:create)
+ should_not allow_value("foo,bar").for(:title).on(:create)
+ should_not allow_value("foo\abar").for(:title).on(:create)
+ should_not allow_value("café").for(:title).on(:create)
+ should_not allow_value("東方").for(:title).on(:create)
+ should_not allow_value("FAV:blah").for(:title).on(:create)
+ should_not allow_value("X"*171).for(:title).on(:create)
+ end
+
context "with other names" do
should "not allow artist wikis to have other names" do
tag = create(:artist_tag)
From 9de7a07af7d6e29e17bfac3b9fae17507fc6b949 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sat, 19 Dec 2020 22:46:40 -0600
Subject: [PATCH 058/132] /status: fix blank distro version field.
The `source` command is a bash-ism and doesn't work in a strictly POSIX
shell like dash, which is the /bin/sh on Debian/Ubuntu. Use `.` instead.
https://en.wikipedia.org/wiki/Dot_(command)
---
app/logical/server_status.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/logical/server_status.rb b/app/logical/server_status.rb
index 197582bf7..e3a903230 100644
--- a/app/logical/server_status.rb
+++ b/app/logical/server_status.rb
@@ -46,7 +46,7 @@ class ServerStatus
end
def distro_version
- `source /etc/os-release; echo "$NAME $VERSION"`.chomp
+ `. /etc/os-release; echo "$NAME $VERSION"`.chomp
end
def libvips_version
From 28926c2332b7fbe1d8e67012b4e5b72f79d631bb Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 20 Dec 2020 00:39:05 -0600
Subject: [PATCH 059/132] autocomplete: remove old autocomplete endpoints.
Remove /tag/autocomplete.json and /saved_searches/labels.json.
---
app/controllers/saved_searches_controller.rb | 6 -
app/controllers/tags_controller.rb | 12 --
app/logical/tag_autocomplete.rb | 113 -----------------
app/models/saved_search.rb | 13 --
config/routes.rb | 12 +-
.../saved_searches_controller_test.rb | 7 --
test/functional/tags_controller_test.rb | 14 ---
test/unit/saved_search_test.rb | 13 --
test/unit/tag_autocomplete_test.rb | 115 ------------------
9 files changed, 2 insertions(+), 303 deletions(-)
delete mode 100644 app/logical/tag_autocomplete.rb
delete mode 100644 test/unit/tag_autocomplete_test.rb
diff --git a/app/controllers/saved_searches_controller.rb b/app/controllers/saved_searches_controller.rb
index a946e34c8..b7a163285 100644
--- a/app/controllers/saved_searches_controller.rb
+++ b/app/controllers/saved_searches_controller.rb
@@ -6,12 +6,6 @@ class SavedSearchesController < ApplicationController
respond_with(@saved_searches)
end
- def labels
- authorize SavedSearch
- @labels = SavedSearch.search_labels(CurrentUser.id, params[:search]).take(params[:limit].to_i || 10)
- respond_with(@labels)
- end
-
def create
@saved_search = authorize SavedSearch.new(user: CurrentUser.user, **permitted_attributes(SavedSearch))
@saved_search.save
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 9fcf23fd7..8deac803c 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -12,18 +12,6 @@ class TagsController < ApplicationController
respond_with(@tags)
end
- def autocomplete
- if CurrentUser.is_builder?
- # limit rollout
- @tags = TagAutocomplete.search(params[:search][:name_matches])
- else
- @tags = Tag.names_matches_with_aliases(params[:search][:name_matches], params.fetch(:limit, 10).to_i)
- end
-
- # XXX
- respond_with(@tags.map(&:attributes))
- end
-
def show
@tag = authorize Tag.find(params[:id])
respond_with(@tag)
diff --git a/app/logical/tag_autocomplete.rb b/app/logical/tag_autocomplete.rb
deleted file mode 100644
index ba34e658c..000000000
--- a/app/logical/tag_autocomplete.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-module TagAutocomplete
- module_function
-
- PREFIX_BOUNDARIES = "(_/:;-"
- LIMIT = 10
-
- Result = Struct.new(:name, :post_count, :category, :antecedent_name, :source) do
- include ActiveModel::Serializers::JSON
- include ActiveModel::Serializers::Xml
-
- def attributes
- (members + [:weight]).map { |x| [x.to_s, send(x)] }.to_h
- end
-
- def weight
- case source
- when :exact then 1.0
- when :prefix then 0.8
- when :alias then 0.2
- when :correct then 0.1
- end
- end
- end
-
- def search(query)
- query = Tag.normalize_name(query)
-
- count_sort(
- search_exact(query, 8) +
- search_prefix(query, 4) +
- search_correct(query, 2) +
- search_aliases(query, 3)
- )
- end
-
- def count_sort(words)
- words.uniq(&:name).sort_by do |x|
- x.post_count * x.weight
- end.reverse.slice(0, LIMIT)
- end
-
- def search_exact(query, n = 4)
- Tag
- .where_like(:name, query + "*")
- .where("post_count > 0")
- .order("post_count desc")
- .limit(n)
- .pluck(:name, :post_count, :category)
- .map {|row| Result.new(*row, nil, :exact)}
- end
-
- def search_correct(query, n = 2)
- if query.size <= 3
- return []
- end
-
- Tag
- .where("name % ?", query)
- .where("abs(length(name) - ?) <= 3", query.size)
- .where_like(:name, query[0] + "*")
- .where("post_count > 0")
- .order(Arel.sql("similarity(name, #{Tag.connection.quote(query)}) DESC"))
- .limit(n)
- .pluck(:name, :post_count, :category)
- .map {|row| Result.new(*row, nil, :correct)}
- end
-
- def search_prefix(query, n = 3)
- if query.size >= 5
- return []
- end
-
- if query.size <= 1
- return []
- end
-
- if query =~ /[-_()]/
- return []
- end
-
- if query.size >= 3
- min_post_count = 0
- else
- min_post_count = 5_000
- n += 2
- end
-
- regexp = "([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)"
- Tag
- .where('regexp_replace(name, ?, ?, ?) like ?', regexp, '\1', 'g', query.to_escaped_for_sql_like + '%')
- .where("post_count > ?", min_post_count)
- .where("post_count > 0")
- .order("post_count desc")
- .limit(n)
- .pluck(:name, :post_count, :category)
- .map {|row| Result.new(*row, nil, :prefix)}
- end
-
- def search_aliases(query, n = 10)
- wildcard_name = query + "*"
- TagAlias
- .select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
- .joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
- .where_like(:antecedent_name, wildcard_name)
- .active
- .where_not_like("tags.name", wildcard_name)
- .where("tags.post_count > 0")
- .order("tags.post_count desc")
- .limit(n)
- .pluck(:name, :post_count, :category, :antecedent_name)
- .map {|row| Result.new(*row, :alias)}
- end
-end
diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb
index e3dad2dc0..fbe92a248 100644
--- a/app/models/saved_search.rb
+++ b/app/models/saved_search.rb
@@ -71,19 +71,6 @@ class SavedSearch < ApplicationRecord
all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
end
- def search_labels(user_id, params)
- labels = labels_for(user_id)
-
- if params[:label].present?
- query = Regexp.escape(params[:label]).gsub("\\*", ".*")
- query = ".*#{query}.*" unless query.include?("*")
- query = /\A#{query}\z/
- labels = labels.grep(query)
- end
-
- labels
- end
-
def labels_for(user_id)
SavedSearch
.where(user_id: user_id)
diff --git a/config/routes.rb b/config/routes.rb
index 6244fed13..13675fad2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -215,21 +215,13 @@ Rails.application.routes.draw do
resource :related_tag, :only => [:show, :update]
resources :recommended_posts, only: [:index]
resources :robots, only: [:index]
- resources :saved_searches, :except => [:show] do
- collection do
- get :labels
- end
- end
+ resources :saved_searches, :except => [:show]
resource :session, only: [:new, :create, :destroy] do
get :sign_out, on: :collection
end
resource :source, :only => [:show]
resource :status, only: [:show], controller: "status"
- resources :tags do
- collection do
- get :autocomplete
- end
- end
+ resources :tags
resources :tag_aliases, only: [:show, :index, :destroy]
resources :tag_implications, only: [:show, :index, :destroy]
resources :uploads do
diff --git a/test/functional/saved_searches_controller_test.rb b/test/functional/saved_searches_controller_test.rb
index bc390dd08..8039d0689 100644
--- a/test/functional/saved_searches_controller_test.rb
+++ b/test/functional/saved_searches_controller_test.rb
@@ -16,13 +16,6 @@ class SavedSearchesControllerTest < ActionDispatch::IntegrationTest
end
end
- context "labels action" do
- should "render" do
- get_auth labels_saved_searches_path, @user, as: :json
- assert_response :success
- end
- end
-
context "create action" do
should "render" do
post_auth saved_searches_path, @user, params: { saved_search: { query: "bkub", label_string: "artist" }}
diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb
index 797412d90..f00bfb7ab 100644
--- a/test/functional/tags_controller_test.rb
+++ b/test/functional/tags_controller_test.rb
@@ -76,20 +76,6 @@ class TagsControllerTest < ActionDispatch::IntegrationTest
end
end
- context "autocomplete action" do
- should "render" do
- get autocomplete_tags_path, params: { search: { name_matches: "t" }, format: :json }
- assert_response :success
- end
-
- should "respect the only param" do
- get autocomplete_tags_path, params: { search: { name_matches: "t", only: "name" }, format: :json }
-
- assert_response :success
- assert_equal "touhou", response.parsed_body.first["name"]
- end
- end
-
context "show action" do
should "render" do
get tag_path(@tag)
diff --git a/test/unit/saved_search_test.rb b/test/unit/saved_search_test.rb
index 75cbcf8f6..d114b90e9 100644
--- a/test/unit/saved_search_test.rb
+++ b/test/unit/saved_search_test.rb
@@ -44,19 +44,6 @@ class SavedSearchTest < ActiveSupport::TestCase
end
end
- context ".search_labels" do
- setup do
- FactoryBot.create(:tag_alias, antecedent_name: "bbb", consequent_name: "ccc", creator: @user)
- FactoryBot.create(:saved_search, user: @user, label_string: "blah", query: "aaa")
- FactoryBot.create(:saved_search, user: @user, label_string: "blahbling", query: "CCC BBB AAA")
- FactoryBot.create(:saved_search, user: @user, label_string: "qux", query: " aaa bbb ccc ")
- end
-
- should "fetch the queries used by a user for a label" do
- assert_equal(%w(blah blahbling), SavedSearch.search_labels(@user.id, label: "blah"))
- end
- end
-
context ".post_ids_for" do
context "with a label" do
setup do
diff --git a/test/unit/tag_autocomplete_test.rb b/test/unit/tag_autocomplete_test.rb
deleted file mode 100644
index f37f930ce..000000000
--- a/test/unit/tag_autocomplete_test.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-require 'test_helper'
-
-class TagAutocompleteTest < ActiveSupport::TestCase
- subject { TagAutocomplete }
-
- context "#search" do
- should "be case insensitive" do
- create(:tag, name: "abcdef", post_count: 1)
- assert_equal(["abcdef"], subject.search("A").map(&:name))
- end
-
- should "not return duplicates" do
- create(:tag, name: "red_eyes", post_count: 5001)
- assert_equal(%w[red_eyes], subject.search("re").map(&:name))
- end
- end
-
- context "#search_exact" do
- setup do
- @tags = [
- create(:tag, name: "abcdef", post_count: 1),
- create(:tag, name: "abczzz", post_count: 2),
- create(:tag, name: "abcyyy", post_count: 0),
- create(:tag, name: "bbbbbb")
- ]
- end
-
- should "find the tags" do
- expected = [
- @tags[1],
- @tags[0]
- ].map(&:name)
- assert_equal(expected, subject.search_exact("abc", 3).map(&:name))
- end
- end
-
- context "#search_correct" do
- setup do
- CurrentUser.stubs(:id).returns(1)
-
- @tags = [
- create(:tag, name: "abcde", post_count: 1),
- create(:tag, name: "abcdz", post_count: 2),
-
- # one char mismatch
- create(:tag, name: "abcez", post_count: 2),
-
- # too long
- create(:tag, name: "abcdefghijk", post_count: 2),
-
- # wrong prefix
- create(:tag, name: "bbcdef", post_count: 2),
-
- # zero post count
- create(:tag, name: "abcdy", post_count: 0),
-
- # completely different
- create(:tag, name: "bbbbb")
- ]
- end
-
- should "find the tags" do
- expected = [
- @tags[0],
- @tags[1],
- @tags[2]
- ].map(&:name)
- assert_equal(expected, subject.search_correct("abcd", 3).map(&:name))
- end
- end
-
- context "#search_prefix" do
- setup do
- @tags = [
- create(:tag, name: "abcdef", post_count: 1),
- create(:tag, name: "alpha_beta_cat", post_count: 2),
- create(:tag, name: "alpha_beta_dat", post_count: 0),
- create(:tag, name: "alpha_beta_(cane)", post_count: 2),
- create(:tag, name: "alpha_beta/cane", post_count: 2)
- ]
- end
-
- should "find the tags" do
- expected = [
- @tags[1],
- @tags[3],
- @tags[4]
- ].map(&:name)
- assert_equal(expected, subject.search_prefix("abc", 3).map(&:name))
- end
- end
-
- context "#search_aliases" do
- setup do
- @user = create(:user)
- @tags = [
- create(:tag, name: "/abc", post_count: 0),
- create(:tag, name: "abcdef", post_count: 1),
- create(:tag, name: "zzzzzz", post_count: 1)
- ]
- as(@user) do
- @aliases = [
- create(:tag_alias, antecedent_name: "/abc", consequent_name: "abcdef", status: "active")
- ]
- end
- end
-
- should "find the tags" do
- results = subject.search_aliases("/abc", 3)
- assert_equal(1, results.size)
- assert_equal("abcdef", results[0].name)
- assert_equal("/abc", results[0].antecedent_name)
- end
- end
-end
From 3ad4beac02693932bc77cabfa38a2ff162295bc8 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 20 Dec 2020 01:27:48 -0600
Subject: [PATCH 060/132] autocomplete: fix exception when completing
unsupported metatags.
---
app/logical/autocomplete_service.rb | 2 ++
test/unit/autocomplete_service_test.rb | 8 ++++++++
2 files changed, 10 insertions(+)
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
index f75101098..23bb33f92 100644
--- a/app/logical/autocomplete_service.rb
+++ b/app/logical/autocomplete_service.rb
@@ -148,6 +148,8 @@ class AutocompleteService
autocomplete_saved_search_label(value)
when *STATIC_METATAGS.keys
autocomplete_static_metatag(metatag, value)
+ else
+ []
end
results.map do |result|
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
index e0463f36e..d57ef52ce 100644
--- a/test/unit/autocomplete_service_test.rb
+++ b/test/unit/autocomplete_service_test.rb
@@ -150,6 +150,14 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
assert_autocomplete_equals(%w[touhou], "touhuo", :tag_query)
end
+ should "ignore unsupported metatags" do
+ assert_autocomplete_equals([], "date:2020", :tag_query)
+ assert_autocomplete_equals([], "score:20", :tag_query)
+ assert_autocomplete_equals([], "favcount:>20", :tag_query)
+ assert_autocomplete_equals([], "age:<1w", :tag_query)
+ assert_autocomplete_equals([], "limit:200", :tag_query)
+ end
+
should "autocomplete static metatags" do
assert_autocomplete_equals(["status:active"], "status:act", :tag_query)
assert_autocomplete_equals(["parent:active"], "parent:act", :tag_query)
From 48ff7c42cd644d8d35b37365c88bcf4653873c12 Mon Sep 17 00:00:00 2001
From: evazion
Date: Sun, 20 Dec 2020 01:33:10 -0600
Subject: [PATCH 061/132] autocomplete: bump opensearch description version.
Fix browsers still using the old autocomplete endpoint for opensearch
searches (searches performed in the browser toolbar).
---
app/views/layouts/default.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index 97174b523..c79fa8acb 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -7,7 +7,7 @@
<%= render "meta_links", collection: @current_item %>
<%= tag.link rel: "canonical", href: canonical_url %>
- <%= tag.link rel: "search", type: "application/opensearchdescription+xml", href: opensearch_url(format: :xml, version: 1), title: "Search posts" %>
+ <%= tag.link rel: "search", type: "application/opensearchdescription+xml", href: opensearch_url(format: :xml, version: 2), title: "Search posts" %>
<%= csrf_meta_tag %>
<% unless CurrentUser.enable_desktop_mode? %>
From 2423c8a4471e6f9307985d66a88316c180ba2b65 Mon Sep 17 00:00:00 2001
From: nonamethanks
Date: Mon, 21 Dec 2020 15:28:28 +0100
Subject: [PATCH 062/132] Weibo: use proxy for upload previews
---
app/logical/sources/strategies/weibo.rb | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/app/logical/sources/strategies/weibo.rb b/app/logical/sources/strategies/weibo.rb
index 4daf5e69e..e953c9ff4 100644
--- a/app/logical/sources/strategies/weibo.rb
+++ b/app/logical/sources/strategies/weibo.rb
@@ -90,6 +90,10 @@ module Sources
image_urls.map { |img| img.gsub(%r{.cn/\w+/(\w+)}, '.cn/orj360/\1') }
end
+ def headers
+ { "Referer" => "https://weibo.com" }
+ end
+
def page_url
if api_response.present?
artist_id = api_response["user"]["id"]
From efb836ac028d49082fa2d7d5879d7de51498eed3 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 03:23:19 -0600
Subject: [PATCH 063/132] wikis: normalize Unicode characters in wiki bodies.
* Introduce an abstraction for normalizing attributes. Very loosely
modeled after https://github.com/fnando/normalize_attributes.
* Normalize wiki bodies to Unicode NFC form.
* Normalize Unicode space characters in wiki bodies (strip zero width
spaces, normalize line endings to CRLF, normalize Unicode spaces to
ASCII spaces).
* Trim spaces from the start and end of wiki page bodies. This may cause
wiki page diffs to show spaces being removed even when the user didn't
explicitly remove the spaces themselves.
---
app/logical/concerns/normalizable.rb | 18 +++++++++++++++++
app/models/application_record.rb | 1 +
app/models/wiki_page.rb | 8 +++-----
config/initializers/core_extensions.rb | 13 ++++++++++++
test/unit/string_test.rb | 28 ++++++++++++++++++++++++++
test/unit/wiki_page_test.rb | 13 ++++++++++++
6 files changed, 76 insertions(+), 5 deletions(-)
create mode 100644 app/logical/concerns/normalizable.rb
diff --git a/app/logical/concerns/normalizable.rb b/app/logical/concerns/normalizable.rb
new file mode 100644
index 000000000..7cad97e61
--- /dev/null
+++ b/app/logical/concerns/normalizable.rb
@@ -0,0 +1,18 @@
+module Normalizable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def normalize(attribute, method_name)
+ define_method("#{attribute}=") do |value|
+ normalized_value = self.class.send(method_name, value)
+ super(normalized_value)
+ end
+ end
+
+ private
+
+ def normalize_text(text)
+ text.unicode_normalize(:nfc).normalize_whitespace.strip
+ end
+ end
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index e162b39dd..fc8391890 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -3,6 +3,7 @@ class ApplicationRecord < ActiveRecord::Base
include Deletable
include Mentionable
+ include Normalizable
extend HasBitFlags
extend Searchable
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 79922b98c..236ba0410 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -3,11 +3,13 @@ class WikiPage < ApplicationRecord
META_WIKIS = ["list_of_", "tag_group:", "pool_group:", "howto:", "about:", "help:", "template:"]
- before_validation :normalize_title
before_validation :normalize_other_names
before_save :update_dtext_links, if: :dtext_links_changed?
after_save :create_version
+ normalize :title, :normalize_title
+ normalize :body, :normalize_text
+
validates :title, tag_name: true, presence: true, uniqueness: true, if: :title_changed?
validates :body, presence: true, unless: -> { is_deleted? || other_names.present? }
validate :validate_rename
@@ -151,10 +153,6 @@ class WikiPage < ApplicationRecord
title.to_s.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
end
- def normalize_title
- self.title = WikiPage.normalize_title(title)
- end
-
def normalize_other_names
self.other_names = other_names.map { |name| WikiPage.normalize_other_name(name) }.uniq
end
diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb
index ef7dba48d..2456c6b9a 100644
--- a/config/initializers/core_extensions.rb
+++ b/config/initializers/core_extensions.rb
@@ -41,6 +41,19 @@ module Danbooru
pattern = Regexp.escape(pattern).gsub(/\\\*/, ".*")
match?(/\A#{pattern}\z/i)
end
+
+ def normalize_whitespace
+ # Normalize various horizontal space characters to ASCII space.
+ text = gsub(/\p{Zs}|\t/, " ")
+
+ # Strip various zero width space characters.
+ text = text.gsub(/[\u180E\u200B\u200C\u200D\u2060\uFEFF]/, "")
+
+ # Normalize various line ending characters to CRLF.
+ text = text.gsub(/\r?\n|\r|\v|\f|\u0085|\u2028|\u2029/, "\r\n")
+
+ text
+ end
end
end
end
diff --git a/test/unit/string_test.rb b/test/unit/string_test.rb
index 47224ba77..247c637e0 100644
--- a/test/unit/string_test.rb
+++ b/test/unit/string_test.rb
@@ -14,4 +14,32 @@ class StringTest < ActiveSupport::TestCase
assert_equal('%*%', '*\**'.to_escaped_for_sql_like)
end
end
+
+ context "String#normalize_whitespace" do
+ should "normalize unicode spaces" do
+ assert_equal("foo bar", "foo bar".normalize_whitespace)
+ assert_equal("foo bar", "foo\u00A0bar".normalize_whitespace)
+ assert_equal("foo bar", "foo\u3000bar".normalize_whitespace)
+ end
+
+ should "strip zero width characters" do
+ assert_equal("foobar", "foo\u180Ebar".normalize_whitespace)
+ assert_equal("foobar", "foo\u200Bbar".normalize_whitespace)
+ assert_equal("foobar", "foo\u200Cbar".normalize_whitespace)
+ assert_equal("foobar", "foo\u200Dbar".normalize_whitespace)
+ assert_equal("foobar", "foo\u2060bar".normalize_whitespace)
+ assert_equal("foobar", "foo\uFEFFbar".normalize_whitespace)
+ end
+
+ should "normalize line endings" do
+ assert_equal("foo\r\nbar", "foo\r\nbar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\nbar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\rbar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\vbar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\fbar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\u0085bar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\u2028bar".normalize_whitespace)
+ assert_equal("foo\r\nbar", "foo\u2029bar".normalize_whitespace)
+ end
+ end
end
diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb
index 32391cfb8..150561e3b 100644
--- a/test/unit/wiki_page_test.rb
+++ b/test/unit/wiki_page_test.rb
@@ -78,6 +78,19 @@ class WikiPageTest < ActiveSupport::TestCase
end
end
+ context "the wiki body" do
+ should "be normalized to NFC" do
+ # \u00E9: é; \u0301: acute accent
+ @wiki = create(:wiki_page, body: "Poke\u0301mon")
+ assert_equal("Pok\u00E9mon", @wiki.body)
+ end
+
+ should "normalize line endings and trim spaces" do
+ @wiki = create(:wiki_page, body: " foo\nbar\n")
+ assert_equal("foo\r\nbar", @wiki.body)
+ end
+ end
+
context "during title validation" do
# these values are allowed because they're normalized first
should allow_value(" foo ").for(:title).on(:create)
From 6ac988271116296ba675020327fb226a70639e60 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 18:48:01 -0600
Subject: [PATCH 064/132] newrelic: log country of each request in newrelic.
Log the country of each HTTP request in NewRelic. Uses the CF-IPCountry
header set by Cloudflare.
---
app/logical/danbooru_logger.rb | 9 ++++++++-
test/unit/session_loader_test.rb | 1 +
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/app/logical/danbooru_logger.rb b/app/logical/danbooru_logger.rb
index 01422f727..09f763e7e 100644
--- a/app/logical/danbooru_logger.rb
+++ b/app/logical/danbooru_logger.rb
@@ -24,7 +24,14 @@ class DanbooruLogger
def self.add_session_attributes(request, session, user)
request_params = request.parameters.with_indifferent_access.except(:controller, :action)
session_params = session.to_h.with_indifferent_access.slice(:session_id, :started_at)
- user_params = { id: user&.id, name: user&.name, level: user&.level_string, ip: request.remote_ip, safe_mode: CurrentUser.safe_mode? }
+ user_params = {
+ id: user&.id,
+ name: user&.name,
+ level: user&.level_string,
+ ip: request.remote_ip,
+ country: request.headers["CF-IPCountry"],
+ safe_mode: CurrentUser.safe_mode?
+ }
add_attributes("request.params", request_params)
add_attributes("session.params", session_params)
diff --git a/test/unit/session_loader_test.rb b/test/unit/session_loader_test.rb
index 075eca261..984ea6cf1 100644
--- a/test/unit/session_loader_test.rb
+++ b/test/unit/session_loader_test.rb
@@ -10,6 +10,7 @@ class SessionLoaderTest < ActiveSupport::TestCase
@request.stubs(:cookie_jar).returns({})
@request.stubs(:parameters).returns({})
@request.stubs(:session).returns({})
+ @request.stubs(:headers).returns({})
SessionLoader.any_instance.stubs(:initialize_session_cookies)
end
From 6c99bbbf47feacce0351da6dfc24a637d8477499 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 18:57:04 -0600
Subject: [PATCH 065/132] posts: limit sources to 1200 chars long.
The longest sources on Danbooru are DeviantArt wixmp.com sources, which
max out at ~900 chars.
---
app/models/post.rb | 1 +
test/unit/post_test.rb | 7 +++++++
2 files changed, 8 insertions(+)
diff --git a/app/models/post.rb b/app/models/post.rb
index 9fd75b7ad..58e7079d3 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -20,6 +20,7 @@ class Post < ApplicationRecord
before_validation :remove_parent_loops
validates_uniqueness_of :md5, :on => :create, message: ->(obj, data) { "duplicate: #{Post.find_by_md5(obj.md5).id}"}
validates_inclusion_of :rating, in: %w(s q e), message: "rating must be s, q, or e"
+ validates :source, length: { maximum: 1200 }
validate :added_tags_are_valid
validate :removed_tags_are_valid
validate :has_artist_tag
diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb
index ee470de05..c2fa690fa 100644
--- a/test/unit/post_test.rb
+++ b/test/unit/post_test.rb
@@ -1076,6 +1076,13 @@ class PostTest < ActiveSupport::TestCase
@post.update(:tag_string => "source:https://img18.pixiv.net/img/evazion/14901720.png")
assert_equal(14901720, @post.pixiv_id)
end
+
+ should "validate the max source length" do
+ @post.update(source: "X"*1201)
+
+ assert_equal(false, @post.valid?)
+ assert_equal(["is too long (maximum is 1200 characters)"], @post.errors[:source])
+ end
end
context "of" do
From 906430b9839ac08f36bc86eb40a37818430d07b3 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 19:16:44 -0600
Subject: [PATCH 066/132] config: add option for customizing session cookie
name.
Fixes getting logged out when you visited Testbooru because of
Testbooru's session cookies clobbering Danbooru's session cookies.
---
config/danbooru_default_config.rb | 6 ++++++
config/initializers/session_store.rb | 10 +++++++++-
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 0d2cc3321..0bb6d3459 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -46,6 +46,12 @@ module Danbooru
"DanbooruBot"
end
+ # The name of the cookie that stores the current user's login session.
+ # Changing this will force all users to login again.
+ def session_cookie_name
+ "_danbooru2_session"
+ end
+
def source_code_url
"https://github.com/danbooru/danbooru"
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 6cb8f0eb3..2bbb72e68 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,11 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.session_store :cookie_store, key: '_danbooru2_session', domain: :all, tld_length: 2, same_site: :lax, secure: Rails.env.production?
+# https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html
+Rails.application.config.session_store(
+ :cookie_store,
+ key: Danbooru.config.session_cookie_name,
+ domain: :all,
+ tld_length: 2,
+ same_site: :lax,
+ secure: Rails.env.production?
+)
From f3880569e15a8e7cbf7a2418b0ff2a812626f2c6 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 20:32:27 -0600
Subject: [PATCH 067/132] rails: update settings to 6.1 defaults.
Most of the new settings aren't relevant to us. We do have to fix some
tests to work around a Rails bug. `assert_enqueued_email_with` uses the
wrong queue, so we have to specify it explicitly. This is fixed in Rails
HEAD but not yet released.
---
config/application.rb | 2 +-
.../new_framework_defaults_6_1.rb | 63 -------------------
test/functional/emails_controller_test.rb | 4 +-
.../password_resets_controller_test.rb | 2 +-
test/functional/users_controller_test.rb | 2 +-
5 files changed, 5 insertions(+), 68 deletions(-)
delete mode 100644 config/initializers/new_framework_defaults_6_1.rb
diff --git a/config/application.rb b/config/application.rb
index 5f2e046ac..85ebe5db0 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,7 +35,7 @@ module Danbooru
config.app_generators.scaffold_controller :responders_controller
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 6.0
+ config.load_defaults 6.1
config.active_record.schema_format = :sql
config.encoding = "utf-8"
config.filter_parameters += [:password, :password_confirmation, :password_hash, :api_key]
diff --git a/config/initializers/new_framework_defaults_6_1.rb b/config/initializers/new_framework_defaults_6_1.rb
deleted file mode 100644
index 629888deb..000000000
--- a/config/initializers/new_framework_defaults_6_1.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 6.1 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Support for inversing belongs_to -> has_many Active Record associations.
-# Rails.application.config.active_record.has_many_inversing = true
-
-# Track Active Storage variants in the database.
-# Rails.application.config.active_storage.track_variants = true
-
-# Apply random variation to the delay when retrying failed jobs.
-# Rails.application.config.active_job.retry_jitter = 0.15
-
-# Stop executing `after_enqueue`/`after_perform` callbacks if
-# `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
-# Rails.application.config.active_job.skip_after_callbacks_if_terminated = true
-
-# Specify cookies SameSite protection level: either :none, :lax, or :strict.
-#
-# This change is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 6.1.
-# Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
-
-# Generate CSRF tokens that are encoded in URL-safe Base64.
-#
-# This change is not backwards compatible with earlier Rails versions.
-# It's best enabled when your entire app is migrated and stable on 6.1.
-# Rails.application.config.action_controller.urlsafe_csrf_tokens = true
-
-# Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
-# UTC offset or a UTC time.
-# ActiveSupport.utc_to_local_returns_utc_offset_times = true
-
-# Change the default HTTP status code to `308` when redirecting non-GET/HEAD
-# requests to HTTPS in `ActionDispatch::SSL` middleware.
-# Rails.application.config.action_dispatch.ssl_default_redirect_status = 308
-
-# Use new connection handling API. For most applications this won't have any
-# effect. For applications using multiple databases, this new API provides
-# support for granular connection swapping.
-# Rails.application.config.active_record.legacy_connection_handling = false
-
-# Make `form_with` generate non-remote forms by default.
-# Rails.application.config.action_view.form_with_generates_remote_forms = false
-
-# Set the default queue name for the analysis job to the queue adapter default.
-# Rails.application.config.active_storage.queues.analysis = nil
-
-# Set the default queue name for the purge job to the queue adapter default.
-# Rails.application.config.active_storage.queues.purge = nil
-
-# Set the default queue name for the incineration job to the queue adapter default.
-# Rails.application.config.action_mailbox.queues.incineration = nil
-
-# Set the default queue name for the routing job to the queue adapter default.
-# Rails.application.config.action_mailbox.queues.routing = nil
-
-# Set the default queue name for the mail deliver job to the queue adapter default.
-# Rails.application.config.action_mailer.deliver_later_queue_name = nil
diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb
index 4da9bb9a5..8d646a382 100644
--- a/test/functional/emails_controller_test.rb
+++ b/test/functional/emails_controller_test.rb
@@ -88,7 +88,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to(settings_path)
assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.email_address.is_verified)
- assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user]
+ assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
end
should "create a new address" do
@@ -101,7 +101,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to(settings_path)
assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.reload.email_address.is_verified)
- assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user]
+ assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
end
end
diff --git a/test/functional/password_resets_controller_test.rb b/test/functional/password_resets_controller_test.rb
index 9455ec67d..16c6c8588 100644
--- a/test/functional/password_resets_controller_test.rb
+++ b/test/functional/password_resets_controller_test.rb
@@ -15,7 +15,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
post password_reset_path, params: { user: { name: @user.name } }
assert_redirected_to new_session_path
- assert_enqueued_email_with UserMailer, :password_reset, args: [@user]
+ assert_enqueued_email_with UserMailer, :password_reset, args: [@user], queue: "default"
end
should "should fail if the user doesn't have a verified email address" do
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index 5c2e2b583..fe4353683 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -268,7 +268,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal("xxx", User.last.name)
assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address)
- assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last]
+ assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last], queue: "default"
end
should "not create a user with an invalid email" do
From 0be9c8dc8b40c59636e7dc636fd750ef811abed9 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 21:05:19 -0600
Subject: [PATCH 068/132] emails: optimize /emails listing.
Fix a suboptimal query that made the /emails page really slow.
---
app/models/email_address.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index ffe6175f1..7897d6335 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -12,7 +12,7 @@ class EmailAddress < ApplicationRecord
def self.visible(user)
if user.is_moderator?
- where(user: User.where("level < ?", user.level)).or(where(user: user))
+ where(user: User.where("level < ?", user.level).or(User.where(id: user.id)))
else
none
end
From db488c247ddea9fb4b547d696fdb0278c4908bd3 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 21:08:54 -0600
Subject: [PATCH 069/132] ip bans: fix deleted field in /ip_bans search form.
Fix the value not being remembered in the search form because we
accidentally used `input_html` instead of `selected`.
---
app/views/ip_bans/index.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/ip_bans/index.html.erb b/app/views/ip_bans/index.html.erb
index b3bdf29db..326f356cc 100644
--- a/app/views/ip_bans/index.html.erb
+++ b/app/views/ip_bans/index.html.erb
@@ -7,7 +7,7 @@
<%= f.input :reason, input_html: { value: params[:search][:reason] } %>
<%= f.input :creator_name, label: "Creator", input_html: { value: params[:search][:creator_name], "data-autocomplete": "user" } %>
<%= f.input :category, collection: IpBan.categories, include_blank: true, selected: params[:search][:category] %>
- <%= f.input :is_deleted, label: "Deleted?", collection: ["Yes", "No"], include_blank: true, input_html: { value: params[:search][:is_deleted] } %>
+ <%= f.input :is_deleted, label: "Status", collection: [["Active", "false"], ["Deleted", "true"]], include_blank: true, selected: params[:search][:is_deleted] %>
<%= f.input :order, collection: [%w[Newest created_at], %w[Oldest created_at_asc], %w[Last\ Seen last_hit_at]], include_blank: true, selected: params[:search][:order] %>
<%= f.submit "Search" %>
<% end %>
From 7a2f72ce98d5e325481f98911bfe47df77711917 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 21:16:16 -0600
Subject: [PATCH 070/132] ip bans: fix /ip_bans listing not showing subnet.
---
app/views/ip_bans/index.html.erb | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/views/ip_bans/index.html.erb b/app/views/ip_bans/index.html.erb
index 326f356cc..7313ec31c 100644
--- a/app/views/ip_bans/index.html.erb
+++ b/app/views/ip_bans/index.html.erb
@@ -14,7 +14,7 @@
<%= table_for @ip_bans, class: "striped autofit", width: "100%" do |t| %>
<% t.column "IP Address" do |ip_ban| %>
- <%= link_to_ip ip_ban.subnetted_ip %>
+ <%= link_to ip_ban.subnetted_ip, ip_address_path(ip_ban.ip_addr.to_s) %>
<% end %>
<% t.column :reason, td: { class: "col-expand" } %>
<% t.column "Status" do |ip_ban| %>
@@ -35,7 +35,6 @@
<%= time_ago_in_words_tagged(ip_ban.created_at) %>
<% end %>
<% t.column column: "control" do |ip_ban| %>
- <%= link_to "Details", ip_address_path(ip_ban.ip_addr.to_s) %> |
<% if ip_ban.is_deleted? %>
<%= link_to "Undelete", ip_ban_path(ip_ban), remote: true, method: :put, "data-params": "ip_ban[is_deleted]=false", "data-confirm": "Are you sure you want to undelete this IP ban?" %>
<% else %>
From 8221c8dcba50ec9fc380109c64e5239b41acdb60 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 21:34:01 -0600
Subject: [PATCH 071/132] users: inline search form on /users index page.
* Add the user search form to the /users page.
* Remove the /users/search page.
---
app/controllers/users_controller.rb | 3 ---
app/views/static/site_map.html.erb | 1 -
app/views/users/_secondary_links.html.erb | 1 -
app/views/users/index.html.erb | 9 +++++++++
app/views/users/search.html.erb | 20 --------------------
config/routes.rb | 1 -
6 files changed, 9 insertions(+), 26 deletions(-)
delete mode 100644 app/views/users/search.html.erb
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e18888579..eb0e2f7b4 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -38,9 +38,6 @@ class UsersController < ApplicationController
respond_with(@users)
end
- def search
- end
-
def show
@user = authorize User.find(params[:id])
respond_with(@user, methods: @user.full_attributes) do |format|
diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index 893ca7b3b..d6a93fdfe 100644
--- a/app/views/static/site_map.html.erb
+++ b/app/views/static/site_map.html.erb
@@ -133,7 +133,6 @@
<% end %>
<%= link_to_wiki "Help", "help:users" %>
<%= link_to("Listing", users_path) %>
- <%= link_to("Search", search_users_path) %>
<%= link_to("Bans", bans_path) %>
<%= link_to("Feedback", user_feedbacks_path) %>
<%= link_to("Terms of Service", terms_of_service_path) %>
diff --git a/app/views/users/_secondary_links.html.erb b/app/views/users/_secondary_links.html.erb
index e1b885966..4487865cd 100644
--- a/app/views/users/_secondary_links.html.erb
+++ b/app/views/users/_secondary_links.html.erb
@@ -1,7 +1,6 @@
<% content_for(:secondary_links) do %>
<%= quick_search_form_for(:name_matches, users_path, "users", autocomplete: "user", redirect: true) %>
<%= subnav_link_to "Listing", users_path %>
- <%= subnav_link_to "Search", search_users_path %>
<% if CurrentUser.user.is_anonymous? %>
<%= subnav_link_to "Sign up", new_user_path %>
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 2c3bae957..beb6c91e8 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -2,6 +2,15 @@
Users
+ <%= search_form_for(users_path) do |f| %>
+ <%= f.input :name_matches, label: "Name", hint: "Use * for wildcard", input_html: { value: params[:search][:name_matches], data: { autocomplete: "user" } } %>
+ <%= f.input :level, collection: User.level_hash.to_a, include_blank: true, selected: params[:search][:level] %>
+ <%= f.input :can_upload_free, label: "Contributor?", as: :select, include_blank: true, selected: params[:search][:can_upload_free] %>
+ <%= f.input :can_approve_posts, label: "Approver?", as: :select, include_blank: true, selected: params[:search][:can_approve_posts] %>
+ <%= f.input :order, collection: [["Joined", "date"], ["Name", "name"], ["Posts", "post_upload_count"], ["Edits", "post_update_count"], ["Notes", "note_count"]], include_blank: true, selected: params[:search][:order] %>
+ <%= f.submit "Search" %>
+ <% end %>
+
<%= table_for @users, width: "100%" do |t| %>
<% t.column column: "control" do |user| %>
<% if policy(CurrentUser.user).promote? %>
diff --git a/app/views/users/search.html.erb b/app/views/users/search.html.erb
deleted file mode 100644
index c41962a0b..000000000
--- a/app/views/users/search.html.erb
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- <%= search_form_for(users_path) do |f| %>
- <%= f.input :name_matches, label: "Name", hint: "Use * for wildcard", input_html: { value: params[:search][:name_matches], data: { autocomplete: "user" } } %>
- <%= f.input :inviter_name, label: "Inviter Name", hint: "Use * for wildcard", input_html: { value: params.dig(:search, :inviter_name), data: { autocomplete: "user" } } %>
-
- <%= f.input :level, collection: User.level_hash.to_a, include_blank: true, selected: params[:search][:level] %>
- <%= f.input :min_level, collection: User.level_hash.to_a, include_blank: true, selected: params[:search][:min_level] %>
- <%= f.input :max_level, collection: User.level_hash.to_a, include_blank: true, selected: params[:search][:max_level] %>
-
- <%= f.input :can_upload_free, label: "Unrestricted uploads?", collection: [%w[Yes true], %w[No false]], include_blank: true, selected: params[:search][:can_upload_free] %>
- <%= f.input :can_approve_posts, label: "Approver?", collection: [%w[Yes true], %w[No false]], include_blank: true, selected: params[:search][:can_approve_posts] %>
-
- <%= f.input :order, collection: [["Join date", "date"], ["Name", "name"], ["Upload count", "post_upload_count"], ["Note count", "note_count"], ["Post update count", "post_update_count"]], selected: params[:search][:order] %>
- <%= f.submit "Search" %>
- <% end %>
-
-
-
-<%= render "secondary_links" %>
diff --git a/config/routes.rb b/config/routes.rb
index 13675fad2..d17321a92 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -243,7 +243,6 @@ Rails.application.routes.draw do
end
collection do
- get :search
get :custom_style
end
end
From 025631ee64eda80aec69fbcc6a113ab04bd98729 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 21:38:48 -0600
Subject: [PATCH 072/132] users: show IPs to mods on /users page.
---
app/views/users/index.html.erb | 28 +++++++++++++---------------
1 file changed, 13 insertions(+), 15 deletions(-)
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index beb6c91e8..625be9e6a 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -11,40 +11,38 @@
<%= f.submit "Search" %>
<% end %>
- <%= table_for @users, width: "100%" do |t| %>
- <% t.column column: "control" do |user| %>
- <% if policy(CurrentUser.user).promote? %>
- <%= link_to "Edit", edit_admin_user_path(user) %>
- <% end %>
- <% end %>
- <% t.column "Name" do |user| %>
+ <%= table_for @users, width: "100%", class: "striped autofit" do |t| %>
+ <% t.column "Name", td: { class: "col-expand" } do |user| %>
<%= link_to_user user %>
- <% if user.inviter %>
- ← <%= link_to_user user.inviter %>
+ <% end %>
+ <% if policy(IpAddress).show? %>
+ <% t.column "IP" do |user| %>
+ <%= link_to user.last_ip_addr, ip_address_path(user.last_ip_addr) %>
<% end %>
<% end %>
<% t.column "Posts" do |user| %>
<%= link_to user.post_upload_count, posts_path(:tags => "user:#{user.name}") %>
<% end %>
- <% t.column "Deleted" do |user| %>
- <%= user.posts.deleted.count %>
+ <% t.column "Edits" do |user| %>
+ <%= link_to user.post_update_count, post_versions_path(:search => {:updater_id => user.id}) %>
<% end %>
<% t.column "Notes" do |user| %>
<%= link_to user.note_update_count, note_versions_path(:search => {:updater_id => user.id}) %>
<% end %>
- <% t.column "Edits" do |user| %>
- <%= link_to user.post_update_count, post_versions_path(:search => {:updater_id => user.id}) %>
- <% end %>
<% t.column "Level" do |user| %>
<%= user.level_string %>
<% end %>
<% t.column "Joined" do |user| %>
<%= compact_time user.created_at %>
<% end %>
+ <% t.column column: "control" do |user| %>
+ <% if policy(CurrentUser.user).promote? %>
+ <%= link_to "Promote", edit_admin_user_path(user) %>
+ <% end %>
+ <% end %>
<% end %>
<%= numbered_paginator(@users) %>
-
From 3c4781f6d842315ba91d887dbf95ca7c100bcbb3 Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 22:32:52 -0600
Subject: [PATCH 073/132] users: update last_logged_in_at hourly.
Update last_logged_in_at on an hourly basis instead of a weekly basis.
---
app/logical/session_loader.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb
index d4ecf45d1..2407d5520 100644
--- a/app/logical/session_loader.rb
+++ b/app/logical/session_loader.rb
@@ -87,7 +87,7 @@ class SessionLoader
def update_last_logged_in_at
return if CurrentUser.is_anonymous?
- return if CurrentUser.last_logged_in_at && CurrentUser.last_logged_in_at > 1.week.ago
+ return if CurrentUser.last_logged_in_at && CurrentUser.last_logged_in_at > 1.hour.ago
CurrentUser.user.update_attribute(:last_logged_in_at, Time.now)
end
From fbb4cfb80762dcb8b199513ef27765d1d829fb0a Mon Sep 17 00:00:00 2001
From: evazion
Date: Mon, 21 Dec 2020 22:40:48 -0600
Subject: [PATCH 074/132] users: let mods see users' last login time.
---
app/policies/user_policy.rb | 4 ++++
app/views/users/_statistics.html.erb | 7 +++++++
app/views/users/index.html.erb | 5 +++++
3 files changed, 16 insertions(+)
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 6ca305635..fa1dff0f4 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -23,6 +23,10 @@ class UserPolicy < ApplicationPolicy
user.is_member?
end
+ def can_see_last_logged_in_at?
+ user.is_moderator?
+ end
+
def can_see_favorites?
user.is_admin? || record.id == user.id || !record.enable_private_favorites?
end
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb
index 2a8ee6ec9..fe708c18c 100644
--- a/app/views/users/_statistics.html.erb
+++ b/app/views/users/_statistics.html.erb
@@ -11,6 +11,13 @@
<%= presenter.join_date %>
+ <% if policy(User).can_see_last_logged_in_at? %>
+
+ Last Seen
+ <%= time_ago_in_words_tagged(user.last_logged_in_at) %>
+
+ <% end %>
+
<% if policy(IpAddress).show? %>
Last IP
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 625be9e6a..fde5ef637 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -32,6 +32,11 @@
<% t.column "Level" do |user| %>
<%= user.level_string %>
<% end %>
+ <% if policy(User).can_see_last_logged_in_at? %>
+ <% t.column "Last Seen" do |user| %>
+ <%= time_ago_in_words_tagged(user.last_logged_in_at) %>
+ <% end %>
+ <% end %>
<% t.column "Joined" do |user| %>
<%= compact_time user.created_at %>
<% end %>
From a084da2dbe460e0081f3f3163082e61110386dc2 Mon Sep 17 00:00:00 2001
From: evazion
Date: Tue, 22 Dec 2020 02:15:21 -0600
Subject: [PATCH 075/132] artists: hide other names of banned artists on index
page.
Don't show other names of banned artists on the /artists page to
anonymous users. Hides potentially sensitive information from Google and
logged out users.
---
app/views/artists/index.html.erb | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/views/artists/index.html.erb b/app/views/artists/index.html.erb
index ed8edf733..4b16fcaf7 100644
--- a/app/views/artists/index.html.erb
+++ b/app/views/artists/index.html.erb
@@ -13,8 +13,10 @@
<% end %>
<% end %>
<% t.column "Other Names", td: {class: "col-expand"} do |artist| %>
- <% artist.other_names.each do |name| %>
- <%= link_to name, artists_path(search: { any_name_matches: name }), class: "artist-other-name", rel: "nofollow" %>
+ <% unless artist.is_banned? && !policy(artist).can_view_banned? %>
+ <% artist.other_names.each do |name| %>
+ <%= link_to name, artists_path(search: { any_name_matches: name }), class: "artist-other-name", rel: "nofollow" %>
+ <% end %>
<% end %>
<% end %>
<% t.column "Status" do |artist| %>
From a947a10c53c79a6d4b2f97862560f965c0fb2581 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 17:51:53 -0600
Subject: [PATCH 076/132] config: add debug_mode option.
Add a debug mode option. This is useful when debugging failed tests.
Debug mode disables parallel testing so you can set breakpoints in tests
with binding.pry (normally parallel testing makes it hard to set
breakpoints).
Debug mode also disables global exception handling for controllers. This
lets exceptions bubble up to the console during controller tests
(normally exceptions are swallowed by the controller, which prevents you
from seeing backtraces in failed controller tests).
---
app/controllers/application_controller.rb | 2 ++
config/danbooru_default_config.rb | 9 +++++++++
test/test_helper.rb | 10 ++++++----
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 8f6faeb53..843b72670 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -88,6 +88,8 @@ class ApplicationController < ActionController::Base
end
def rescue_exception(exception)
+ return if Danbooru.config.debug_mode
+
case exception
when ActionView::Template::Error
rescue_exception(exception.cause)
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 0bb6d3459..beed3096d 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -52,6 +52,15 @@ module Danbooru
"_danbooru2_session"
end
+ # Debug mode does some things to make testing easier. It disables parallel
+ # testing and it replaces Danbooru's custom exception page with the default
+ # Rails exception page. This is only useful during development and testing.
+ #
+ # Usage: `DANBOORU_DEBUG_MODE=true bin/rails test
+ def debug_mode
+ false
+ end
+
def source_code_url
"https://github.com/danbooru/danbooru"
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 0959a8ccd..1af778120 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -30,11 +30,13 @@ class ActiveSupport::TestCase
mock_post_version_service!
mock_pool_version_service!
- parallelize
- parallelize_setup do |worker|
- Rails.application.load_seed
+ unless Danbooru.config.debug_mode
+ parallelize
+ parallelize_setup do |worker|
+ Rails.application.load_seed
- SimpleCov.command_name "#{SimpleCov.command_name}-#{worker}"
+ SimpleCov.command_name "#{SimpleCov.command_name}-#{worker}"
+ end
end
parallelize_teardown do |worker|
From dbb66ace9022872791b72c2ca6fa2d4a1ece425a Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 13:19:03 -0600
Subject: [PATCH 077/132] routes: replace hardcoded routes in models with route
helpers.
Add a Routes module that gives models access to route helpers outside of
views, and use it to replace various hardcoded routes.
---
app/logical/d_text.rb | 6 +++---
app/logical/routes.rb | 11 +++++++++++
app/logical/sources/strategies/pixiv.rb | 6 +++---
app/logical/upload_service/replacer.rb | 2 +-
app/logical/user_promotion.rb | 6 +++---
app/models/bulk_update_request.rb | 2 +-
app/models/comment.rb | 2 +-
app/models/forum_post.rb | 2 +-
app/models/post.rb | 6 ++++--
app/models/user_feedback.rb | 6 +++---
app/models/wiki_page.rb | 2 +-
test/unit/sources/pixiv_test.rb | 2 +-
test/unit/user_feedback_test.rb | 2 +-
13 files changed, 34 insertions(+), 21 deletions(-)
create mode 100644 app/logical/routes.rb
diff --git a/app/logical/d_text.rb b/app/logical/d_text.rb
index d0982a6b8..f665c4923 100644
--- a/app/logical/d_text.rb
+++ b/app/logical/d_text.rb
@@ -109,11 +109,11 @@ class DText
end
if obj.is_approved?
- "The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} has been approved by <@#{obj.approver.name}>.\n\n#{embedded_script}"
+ "The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} has been approved by <@#{obj.approver.name}>.\n\n#{embedded_script}"
elsif obj.is_pending?
- "The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} is pending approval.\n\n#{embedded_script}"
+ "The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} is pending approval.\n\n#{embedded_script}"
elsif obj.is_rejected?
- "The \"bulk update request ##{obj.id}\":/bulk_update_requests/#{obj.id} has been rejected.\n\n#{embedded_script}"
+ "The \"bulk update request ##{obj.id}\":#{Routes.bulk_update_request_path(obj)} has been rejected.\n\n#{embedded_script}"
end
end
end
diff --git a/app/logical/routes.rb b/app/logical/routes.rb
new file mode 100644
index 000000000..8af010188
--- /dev/null
+++ b/app/logical/routes.rb
@@ -0,0 +1,11 @@
+# Allow Rails URL helpers to be used outside of views.
+# Example: Routes.posts_path(tags: "touhou") => /posts?tags=touhou
+
+class Routes
+ include Singleton
+ include Rails.application.routes.url_helpers
+
+ class << self
+ delegate_missing_to :instance
+ end
+end
diff --git a/app/logical/sources/strategies/pixiv.rb b/app/logical/sources/strategies/pixiv.rb
index 25700d0fd..00d5ff86b 100644
--- a/app/logical/sources/strategies/pixiv.rb
+++ b/app/logical/sources/strategies/pixiv.rb
@@ -65,15 +65,15 @@ module Sources
text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match|
pixiv_id = $1
- %(pixiv ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}])
+ %(pixiv ##{pixiv_id} "»":[#{Routes.posts_path(tags: "pixiv:#{pixiv_id}")}])
end
text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match|
member_id = $1
profile_url = "https://www.pixiv.net/users/#{member_id}"
- search_params = {"search[url_matches]" => profile_url}.to_param
+ artist_search_url = Routes.artists_path(search: { url_matches: profile_url })
- %("user/#{member_id}":[#{profile_url}] "»":[/artists?#{search_params}])
+ %("user/#{member_id}":[#{profile_url}] "»":[#{artist_search_url}])
end
text = text.gsub(/\r\n|\r|\n/, " ")
diff --git a/app/logical/upload_service/replacer.rb b/app/logical/upload_service/replacer.rb
index f64198c5a..2a8d8b62b 100644
--- a/app/logical/upload_service/replacer.rb
+++ b/app/logical/upload_service/replacer.rb
@@ -11,7 +11,7 @@ class UploadService
end
def comment_replacement_message(post, replacement)
- %("#{replacement.creator.name}":[/users/#{replacement.creator.id}] replaced this post with a new image:\n\n#{replacement_message(post, replacement)})
+ %("#{replacement.creator.name}":[#{Routes.user_path(replacement.creator)}] replaced this post with a new image:\n\n#{replacement_message(post, replacement)})
end
def replacement_message(post, replacement)
diff --git a/app/logical/user_promotion.rb b/app/logical/user_promotion.rb
index e9653bb2d..6420db5e3 100644
--- a/app/logical/user_promotion.rb
+++ b/app/logical/user_promotion.rb
@@ -32,16 +32,16 @@ class UserPromotion
def create_mod_actions
if old_can_approve_posts != user.can_approve_posts?
- ModAction.log("\"#{promoter.name}\":/users/#{promoter.id} changed approval privileges for \"#{user.name}\":/users/#{user.id} from #{old_can_approve_posts} to [b]#{user.can_approve_posts?}[/b]", :user_approval_privilege)
+ ModAction.log("\"#{promoter.name}\":#{Routes.user_path(promoter)} changed approval privileges for \"#{user.name}\":#{Routes.user_path(user)} from #{old_can_approve_posts} to [b]#{user.can_approve_posts?}[/b]", :user_approval_privilege, promoter)
end
if old_can_upload_free != user.can_upload_free?
- ModAction.log("\"#{promoter.name}\":/users/#{promoter.id} changed unlimited upload privileges for \"#{user.name}\":/users/#{user.id} from #{old_can_upload_free} to [b]#{user.can_upload_free?}[/b]", :user_upload_privilege)
+ ModAction.log("\"#{promoter.name}\":#{Routes.user_path(promoter)} changed unlimited upload privileges for \"#{user.name}\":#{Routes.user_path(user)} from #{old_can_upload_free} to [b]#{user.can_upload_free?}[/b]", :user_upload_privilege, promoter)
end
if user.level_changed?
category = is_upgrade ? :user_account_upgrade : :user_level_change
- ModAction.log(%{"#{user.name}":/users/#{user.id} level changed #{user.level_string_was} -> #{user.level_string}}, category)
+ ModAction.log(%{"#{user.name}":#{Routes.user_path(user)} level changed #{user.level_string_was} -> #{user.level_string}}, category, promoter)
end
end
diff --git a/app/models/bulk_update_request.rb b/app/models/bulk_update_request.rb
index d7afa226d..b40bcabca 100644
--- a/app/models/bulk_update_request.rb
+++ b/app/models/bulk_update_request.rb
@@ -89,7 +89,7 @@ class BulkUpdateRequest < ApplicationRecord
end
def bulk_update_request_link
- %{"bulk update request ##{id}":/bulk_update_requests?search%5Bid%5D=#{id}}
+ %{"bulk update request ##{id}":#{Routes.bulk_update_requests_path(search: { id: id })}}
end
end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 2207706ef..c56271f56 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -21,7 +21,7 @@ class Comment < ApplicationRecord
mentionable(
:message_field => :body,
:title => ->(user_name) {"#{creator.name} mentioned you in a comment on post ##{post_id}"},
- :body => ->(user_name) {"@#{creator.name} mentioned you in a \"comment\":/posts/#{post_id}#comment-#{id} on post ##{post_id}:\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n"}
+ :body => ->(user_name) {"@#{creator.name} mentioned you in a \"comment\":#{Routes.post_path(post, anchor: "comment-#{id}")} on post ##{post_id}:\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n"}
)
module SearchMethods
diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb
index c8aad7962..8cab6029b 100644
--- a/app/models/forum_post.rb
+++ b/app/models/forum_post.rb
@@ -28,7 +28,7 @@ class ForumPost < ApplicationRecord
mentionable(
:message_field => :body,
:title => ->(user_name) {%{#{creator.name} mentioned you in topic ##{topic_id} (#{topic.title})}},
- :body => ->(user_name) {%{@#{creator.name} mentioned you in topic ##{topic_id} ("#{topic.title}":[/forum_topics/#{topic_id}?page=#{forum_topic_page}]):\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n}}
+ :body => ->(user_name) {%{@#{creator.name} mentioned you in topic ##{topic_id} ("#{topic.title}":[#{Routes.forum_topic_path(topic, page: forum_topic_page)}]):\n\n[quote]\n#{DText.extract_mention(body, "@" + user_name)}\n[/quote]\n}}
)
module SearchMethods
diff --git a/app/models/post.rb b/app/models/post.rb
index 58e7079d3..e89d79e34 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1390,7 +1390,8 @@ class Post < ApplicationRecord
new_artist_tags.each do |tag|
if tag.artist.blank?
- warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[/artists/new?artist%5Bname%5D=#{CGI.escape(tag.name)}]")
+ new_artist_path = Routes.new_artist_path(artist: { name: tag.name })
+ warnings.add(:base, "Artist [[#{tag.name}]] requires an artist entry. \"Create new artist entry\":[#{new_artist_path}]")
end
end
end
@@ -1412,7 +1413,8 @@ class Post < ApplicationRecord
return if tags.any?(&:artist?)
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null)
- warnings.add(:base, "Artist tag is required. \"Create new artist tag\":[/artists/new?artist%5Bsource%5D=#{CGI.escape(source)}]. Ask on the forum if you need naming help")
+ new_artist_path = Routes.new_artist_path(artist: { source: source })
+ warnings.add(:base, "Artist tag is required. \"Create new artist tag\":[#{new_artist_path}]. Ask on the forum if you need naming help")
end
def has_copyright_tag
diff --git a/app/models/user_feedback.rb b/app/models/user_feedback.rb
index cda52916c..75a0f7dc3 100644
--- a/app/models/user_feedback.rb
+++ b/app/models/user_feedback.rb
@@ -8,10 +8,10 @@ class UserFeedback < ApplicationRecord
validates_inclusion_of :category, :in => %w(positive negative neutral)
after_create :create_dmail, unless: :disable_dmail_notification
after_update(:if => ->(rec) { CurrentUser.id != rec.creator_id}) do |rec|
- ModAction.log(%{#{CurrentUser.name} updated user feedback for "#{rec.user.name}":/users/#{rec.user_id}}, :user_feedback_update)
+ ModAction.log(%{#{CurrentUser.name} updated user feedback for "#{rec.user.name}":#{Routes.user_path(rec.user)}}, :user_feedback_update)
end
after_destroy(:if => ->(rec) { CurrentUser.id != rec.creator_id}) do |rec|
- ModAction.log(%{#{CurrentUser.name} deleted user feedback for "#{rec.user.name}":/users/#{rec.user_id}}, :user_feedback_delete)
+ ModAction.log(%{#{CurrentUser.name} deleted user feedback for "#{rec.user.name}":#{Routes.user_path(rec.user)}}, :user_feedback_delete)
end
deletable
@@ -52,7 +52,7 @@ class UserFeedback < ApplicationRecord
end
def create_dmail
- body = %{#{disclaimer}@#{creator.name} created a "#{category} record":/user_feedbacks?search[user_id]=#{user_id} for your account:\n\n#{self.body}}
+ body = %{#{disclaimer}@#{creator.name} created a "#{category} record":#{Routes.user_feedbacks_path(search: { user_id: user_id })} for your account:\n\n#{self.body}}
Dmail.create_automated(:to_id => user_id, :title => "Your user record has been updated", :body => body)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 236ba0410..a38f782d0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -122,7 +122,7 @@ class WikiPage < ApplicationRecord
broken_wikis = WikiPage.linked_to(title_was)
if broken_wikis.count > 0
- broken_wiki_search = Rails.application.routes.url_helpers.wiki_pages_path(search: { linked_to: title_was })
+ broken_wiki_search = Routes.wiki_pages_path(search: { linked_to: title_was })
warnings.add(:base, %!Warning: [[#{title_was}]] is still linked from "#{broken_wikis.count} #{"other wiki page".pluralize(broken_wikis.count)}":[#{broken_wiki_search}]. Update #{(broken_wikis.count > 1) ? "these wikis" : "this wiki"} to link to [[#{title}]] instead!)
end
end
diff --git a/test/unit/sources/pixiv_test.rb b/test/unit/sources/pixiv_test.rb
index a68d75ad0..a69106c3d 100644
--- a/test/unit/sources/pixiv_test.rb
+++ b/test/unit/sources/pixiv_test.rb
@@ -195,7 +195,7 @@ module Sources
should "convert illust links and member links to dtext" do
get_source("https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63421642")
- dtext_desc = %(foo 【pixiv #46337015 "»":[/posts?tags=pixiv:46337015]】bar 【pixiv #14901720 "»":[/posts?tags=pixiv:14901720]】\n\nbaz【"user/83739":[https://www.pixiv.net/users/83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F83739]】)
+ dtext_desc = %(foo 【pixiv #46337015 "»":[/posts?tags=pixiv%3A46337015]】bar 【pixiv #14901720 "»":[/posts?tags=pixiv%3A14901720]】\n\nbaz【"user/83739":[https://www.pixiv.net/users/83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F83739]】)
assert_equal(dtext_desc, @site.dtext_artist_commentary_desc)
end
end
diff --git a/test/unit/user_feedback_test.rb b/test/unit/user_feedback_test.rb
index 516a79fd2..5f20faa66 100644
--- a/test/unit/user_feedback_test.rb
+++ b/test/unit/user_feedback_test.rb
@@ -7,7 +7,7 @@ class UserFeedbackTest < ActiveSupport::TestCase
gold = FactoryBot.create(:gold_user)
member = FactoryBot.create(:user)
dmail = <<~EOS.chomp
- @#{gold.name} created a "positive record":/user_feedbacks?search[user_id]=#{user.id} for your account:
+ @#{gold.name} created a "positive record":/user_feedbacks?search%5Buser_id%5D=#{user.id} for your account:
good job!
EOS
From ca742db07a15f7e4b8f10e8f091acc2c4a3b745d Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 19:48:35 -0600
Subject: [PATCH 078/132] routes: remove legacy /user/index and /artist/index
API endpoints.
These endpoints get zero traffic.
---
app/controllers/legacy_controller.rb | 8 --------
app/models/user.rb | 9 ---------
app/views/legacy/artists.json.erb | 1 -
app/views/legacy/artists.xml.erb | 6 ------
app/views/legacy/users.json.erb | 1 -
app/views/legacy/users.xml.erb | 6 ------
config/routes.rb | 4 ----
7 files changed, 35 deletions(-)
delete mode 100644 app/views/legacy/artists.json.erb
delete mode 100644 app/views/legacy/artists.xml.erb
delete mode 100644 app/views/legacy/users.json.erb
delete mode 100644 app/views/legacy/users.xml.erb
diff --git a/app/controllers/legacy_controller.rb b/app/controllers/legacy_controller.rb
index 5557a3146..784b19443 100644
--- a/app/controllers/legacy_controller.rb
+++ b/app/controllers/legacy_controller.rb
@@ -17,18 +17,10 @@ class LegacyController < ApplicationController
end
end
- def users
- @users = User.limit(100).search(params).paginate(params[:page])
- end
-
def tags
@tags = Tag.limit(100).search(params).paginate(params[:page], :limit => params[:limit])
end
- def artists
- @artists = Artist.limit(100).search(search_params).paginate(params[:page])
- end
-
def unavailable
render :plain => "this resource is no longer available", :status => 410
end
diff --git a/app/models/user.rb b/app/models/user.rb
index c56a19adb..071c42d04 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -483,15 +483,6 @@ class User < ApplicationRecord
]
end
- def to_legacy_json
- return {
- "name" => name,
- "id" => id,
- "level" => level,
- "created_at" => created_at.strftime("%Y-%m-%d %H:%M")
- }.to_json
- end
-
def api_token
api_key.try(:key)
end
diff --git a/app/views/legacy/artists.json.erb b/app/views/legacy/artists.json.erb
deleted file mode 100644
index f7b4addaa..000000000
--- a/app/views/legacy/artists.json.erb
+++ /dev/null
@@ -1 +0,0 @@
-[<%= raw @artists.map(&:to_json).join(",") %>]
diff --git a/app/views/legacy/artists.xml.erb b/app/views/legacy/artists.xml.erb
deleted file mode 100644
index 60b65e14e..000000000
--- a/app/views/legacy/artists.xml.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- <% @artists.each do |artist| %>
- " is_active="<%= !artist.is_deleted? %>" name="<%= artist.name %>" updater_id="0" id="<%= artist.id %>" version="0"/>
- <% end %>
-
diff --git a/app/views/legacy/users.json.erb b/app/views/legacy/users.json.erb
deleted file mode 100644
index 296d0b8e0..000000000
--- a/app/views/legacy/users.json.erb
+++ /dev/null
@@ -1 +0,0 @@
-[<%= @users.map {|x| x.to_legacy_json}.join(", ").html_safe %>]
diff --git a/app/views/legacy/users.xml.erb b/app/views/legacy/users.xml.erb
deleted file mode 100644
index fe0dcb2c1..000000000
--- a/app/views/legacy/users.xml.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- <% @users.each do |user| %>
-
- <% end %>
-
diff --git a/config/routes.rb b/config/routes.rb
index d17321a92..8c5e55540 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -262,8 +262,6 @@ Rails.application.routes.draw do
# legacy aliases
get "/artist" => redirect {|params, req| "/artists?page=#{req.params[:page]}&search[name]=#{CGI.escape(req.params[:name].to_s)}"}
- get "/artist/index.xml", :controller => "legacy", :action => "artists", :format => "xml"
- get "/artist/index.json", :controller => "legacy", :action => "artists", :format => "json"
get "/artist/index" => redirect {|params, req| "/artists?page=#{req.params[:page]}"}
get "/artist/show/:id" => redirect("/artists/%{id}")
get "/artist/show" => redirect {|params, req| "/artists?name=#{CGI.escape(req.params[:name].to_s)}"}
@@ -335,8 +333,6 @@ Rails.application.routes.draw do
get "/tag_implication" => redirect {|params, req| "/tag_implications?search[name_matches]=#{CGI.escape(req.params[:query].to_s)}"}
- get "/user/index.xml", :controller => "legacy", :action => "users", :format => "xml"
- get "/user/index.json", :controller => "legacy", :action => "users", :format => "json"
get "/user" => redirect {|params, req| "/users?page=#{req.params[:page]}"}
get "/user/index" => redirect {|params, req| "/users?page=#{req.params[:page]}"}
get "/user/show/:id" => redirect("/users/%{id}")
From a1cd9d2b5c3aba5bd5fd9cde0f7b91599b5a17d8 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 20:19:06 -0600
Subject: [PATCH 079/132] routes: remove unused Danbooru 1 redirects.
Remove various redirects for old Danbooru 1 links. Most of these
received little to no traffic and were only used in a small number of
places in old comments or forum posts.
---
config/routes.rb | 78 +++++++-----------------------------------------
1 file changed, 11 insertions(+), 67 deletions(-)
diff --git a/config/routes.rb b/config/routes.rb
index 8c5e55540..27bb9c97d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -260,96 +260,40 @@ Rails.application.routes.draw do
end
end
- # legacy aliases
+ # Legacy Danbooru 1 redirects.
get "/artist" => redirect {|params, req| "/artists?page=#{req.params[:page]}&search[name]=#{CGI.escape(req.params[:name].to_s)}"}
- get "/artist/index" => redirect {|params, req| "/artists?page=#{req.params[:page]}"}
get "/artist/show/:id" => redirect("/artists/%{id}")
get "/artist/show" => redirect {|params, req| "/artists?name=#{CGI.escape(req.params[:name].to_s)}"}
- get "/artist/history/:id" => redirect("/artist_versions?search[artist_id]=%{id}")
- get "/artist/recent_changes" => redirect("/artist_versions")
-
- get "/comment" => redirect {|params, req| "/comments?page=#{req.params[:page]}"}
- get "/comment/index" => redirect {|params, req| "/comments?page=#{req.params[:page]}"}
- get "/comment/show/:id" => redirect("/comments/%{id}")
- get "/comment/new" => redirect("/comments")
- get("/comment/search" => redirect do |params, req|
- if req.params[:query] =~ /^user:(.+)/i
- "/comments?group_by=comment&search[creator_name]=#{CGI.escape($1)}"
- else
- "/comments/search"
- end
- end)
-
- get "/favorite" => redirect {|params, req| "/favorites?page=#{req.params[:page]}"}
- get "/favorite/index" => redirect {|params, req| "/favorites?page=#{req.params[:page]}"}
- get "/favorite/list_users.json", :controller => "legacy", :action => "unavailable"
get "/forum" => redirect {|params, req| "/forum_topics?page=#{req.params[:page]}"}
- get "/forum/index" => redirect {|params, req| "/forum_topics?page=#{req.params[:page]}"}
get "/forum/show/:id" => redirect {|params, req| "/forum_posts/#{req.params[:id]}?page=#{req.params[:page]}"}
- get "/forum/search" => redirect("/forum_posts/search")
- get "/help/:title" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape('help:' + req.params[:title])}"}
-
- get "/note" => redirect {|params, req| "/notes?page=#{req.params[:page]}"}
- get "/note/index" => redirect {|params, req| "/notes?page=#{req.params[:page]}"}
- get "/note/history" => redirect {|params, req| "/note_versions?search[updater_id]=#{req.params[:user_id]}"}
-
- get "/pool" => redirect {|params, req| "/pools?page=#{req.params[:page]}"}
- get "/pool/index" => redirect {|params, req| "/pools?page=#{req.params[:page]}"}
get "/pool/show/:id" => redirect("/pools/%{id}")
- get "/pool/history/:id" => redirect("/pool_versions?search[pool_id]=%{id}")
- get "/pool/recent_changes" => redirect("/pool_versions")
- get "/post/index.xml", :controller => "legacy", :action => "posts", :format => "xml"
- get "/post/index.json", :controller => "legacy", :action => "posts", :format => "json"
- get "/post/piclens", :controller => "legacy", :action => "unavailable"
get "/post/index" => redirect {|params, req| "/posts?tags=#{CGI.escape(req.params[:tags].to_s)}&page=#{req.params[:page]}"}
- get "/post" => redirect {|params, req| "/posts?tags=#{CGI.escape(req.params[:tags].to_s)}&page=#{req.params[:page]}"}
- get "/post/upload" => redirect("/uploads/new")
- get "/post/moderate" => redirect("/moderator/post/queue")
get "/post/atom" => redirect {|params, req| "/posts.atom?tags=#{CGI.escape(req.params[:tags].to_s)}"}
- get "/post/atom.feed" => redirect {|params, req| "/posts.atom?tags=#{CGI.escape(req.params[:tags].to_s)}"}
- get "/post/popular_by_day" => redirect("/explore/posts/popular")
- get "/post/popular_by_week" => redirect("/explore/posts/popular")
- get "/post/popular_by_month" => redirect("/explore/posts/popular")
get "/post/show/:id/:tag_title" => redirect("/posts/%{id}")
get "/post/show/:id" => redirect("/posts/%{id}")
- get "/post/show" => redirect {|params, req| "/posts?md5=#{req.params[:md5]}"}
- get "/post/view/:id/:tag_title" => redirect("/posts/%{id}")
- get "/post/view/:id" => redirect("/posts/%{id}")
- get "/post/flag/:id" => redirect("/posts/%{id}")
- get("/post_tag_history" => redirect do |params, req|
- page = req.params[:before_id].present? ? "b#{req.params[:before_id]}" : req.params[:page]
- "/post_versions?page=#{page}&search[updater_id]=#{req.params[:user_id]}"
- end)
- get "/post_tag_history/index" => redirect {|params, req| "/post_versions?page=#{req.params[:page]}&search[post_id]=#{req.params[:post_id]}"}
-
- get "/tag/index.xml", :controller => "legacy", :action => "tags", :format => "xml"
- get "/tag/index.json", :controller => "legacy", :action => "tags", :format => "json"
get "/tag" => redirect {|params, req| "/tags?page=#{req.params[:page]}&search[name_matches]=#{CGI.escape(req.params[:name].to_s)}&search[order]=#{req.params[:order]}&search[category]=#{req.params[:type]}"}
get "/tag/index" => redirect {|params, req| "/tags?page=#{req.params[:page]}&search[name_matches]=#{CGI.escape(req.params[:name].to_s)}&search[order]=#{req.params[:order]}"}
- get "/tag_implication" => redirect {|params, req| "/tag_implications?search[name_matches]=#{CGI.escape(req.params[:query].to_s)}"}
-
- get "/user" => redirect {|params, req| "/users?page=#{req.params[:page]}"}
- get "/user/index" => redirect {|params, req| "/users?page=#{req.params[:page]}"}
get "/user/show/:id" => redirect("/users/%{id}")
- get "/user/login" => redirect("/sessions/new")
- get "/user_record" => redirect {|params, req| "/user_feedbacks?search[user_id]=#{req.params[:user_id]}"}
+
+ get "/wiki/show" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape(req.params[:title].to_s)}"}
+ get "/help/:title" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape('help:' + req.params[:title])}"}
+
+ # Legacy Danbooru 1 API endpoints
+ get "/tag/index.xml", :controller => "legacy", :action => "tags", :format => "xml"
+ get "/tag/index.json", :controller => "legacy", :action => "tags", :format => "json"
+ get "/post/index.xml", :controller => "legacy", :action => "posts", :format => "xml"
+ get "/post/index.json", :controller => "legacy", :action => "posts", :format => "json"
+
get "/login", to: "sessions#new", as: :login
get "/logout", to: "sessions#sign_out", as: :logout
get "/profile", to: "users#profile", as: :profile
get "/settings", to: "users#settings", as: :settings
- get "/wiki" => redirect {|params, req| "/wiki_pages?page=#{req.params[:page]}"}
- get "/wiki/index" => redirect {|params, req| "/wiki_pages?page=#{req.params[:page]}"}
- get "/wiki/rename" => redirect("/wiki_pages")
- get "/wiki/show" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape(req.params[:title].to_s)}"}
- get "/wiki/recent_changes" => redirect {|params, req| "/wiki_page_versions?search[updater_id]=#{req.params[:user_id]}"}
- get "/wiki/history/:title" => redirect("/wiki_page_versions?title=%{title}")
-
get "/sitemap" => "static#sitemap_index"
get "/opensearch" => "static#opensearch", :as => "opensearch"
get "/privacy" => "static#privacy_policy", :as => "privacy_policy"
From 039ccfa3af8c9f77402bf2847afbeb73b23111d8 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 22:31:46 -0600
Subject: [PATCH 080/132] routes: optimize route order.
Put the most used routes at the top of the file to optimize route
performance.
---
config/routes.rb | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/config/routes.rb b/config/routes.rb
index 27bb9c97d..94cd168a7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,11 @@
Rails.application.routes.draw do
+ resources :posts, only: [:index, :show, :update, :destroy]
+ resources :autocomplete, only: [:index]
+
+ # XXX This comes *after* defining posts above because otherwise the paginator
+ # generates `/?page=2` instead of `/posts?page=2` on the posts#index page.
+ root "posts#index"
+
namespace :admin do
resources :users, :only => [:edit, :update]
resource :dashboard, :only => [:show]
@@ -67,7 +74,6 @@ Rails.application.routes.draw do
get :search
end
end
- resources :autocomplete, only: [:index]
resources :bans
resources :bulk_update_requests do
member do
@@ -171,7 +177,9 @@ Rails.application.routes.draw do
end
resources :post_replacements, :only => [:index, :new, :create, :update]
resources :post_votes, only: [:index]
- resources :posts, only: [:index, :show, :update, :destroy] do
+
+ # XXX Use `only: []` to avoid redefining post routes defined at top of file.
+ resources :posts, only: [] do
resources :events, :only => [:index], :controller => "post_events"
resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements"
resource :artist_commentary, :only => [:index, :show] do
@@ -313,7 +321,5 @@ Rails.application.routes.draw do
get "/mock/iqdbs/similar" => "mock_services#iqdbs_similar", as: "mock_iqdbs_similar"
post "/mock/iqdbs/similar" => "mock_services#iqdbs_similar"
- root :to => "posts#index"
-
get "*other", :to => "static#not_found"
end
From c17678d509f35b85f1955ae559dfbc1abdb4bd4f Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 24 Dec 2020 00:07:19 -0600
Subject: [PATCH 081/132] routes: add a new 404 page.
* Fix a bug where non-GET 404 requests weren't handled.
* Fix a bug where non-HTML 404 requests weren't handled.
* Show a random image from a specified pool on the 404 page.
---
app/controllers/application_controller.rb | 6 ++--
app/controllers/static_controller.rb | 10 +++++--
app/views/static/not_found.html.erb | 26 +++++++++++++++++
config/danbooru_default_config.rb | 5 ++++
config/routes.rb | 2 +-
public/404.html | 15 ----------
public/500.html | 15 ----------
test/functional/static_controller_test.rb | 34 ++++++++++++++++++++++-
8 files changed, 76 insertions(+), 37 deletions(-)
create mode 100644 app/views/static/not_found.html.erb
delete mode 100644 public/404.html
delete mode 100644 public/500.html
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 843b72670..ad4f918b0 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -124,17 +124,17 @@ class ApplicationController < ActionController::Base
end
end
- def render_error_page(status, exception, message: exception.message, template: "static/error", format: request.format.symbol)
+ def render_error_page(status, exception = nil, message: exception.message, template: "static/error", format: request.format.symbol)
@exception = exception
@expected = status < 500
@message = message.encode("utf-8", invalid: :replace, undef: :replace)
- @backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace)
+ @backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) if @exception
format = :html unless format.in?(%i[html json xml js atom])
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
layout = CurrentUser.user.present? ? "default" : "blank"
- DanbooruLogger.log(@exception, expected: @expected)
+ DanbooruLogger.log(@exception, expected: @expected) if @exception
render template, layout: layout, status: status, formats: format
rescue ActionView::MissingTemplate
render "static/error", layout: layout, status: status, formats: format
diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb
index 709be20f3..331f20a42 100644
--- a/app/controllers/static_controller.rb
+++ b/app/controllers/static_controller.rb
@@ -1,4 +1,6 @@
class StaticController < ApplicationController
+ respond_to :html, :json, :xml
+
def privacy_policy
end
@@ -6,7 +8,11 @@ class StaticController < ApplicationController
end
def not_found
- render plain: "not found", status: :not_found
+ @pool = Pool.find(Danbooru.config.page_not_found_pool_id) if Danbooru.config.page_not_found_pool_id.present?
+ @post = @pool.posts.sample if @pool.present?
+ @artist = @post.tags.select(&:artist?).first if @post.present?
+
+ render_error_page(404, nil, template: "static/not_found", message: "Page not found")
end
def error
@@ -38,7 +44,7 @@ class StaticController < ApplicationController
@search = { is_deleted: "false" }
when "posts"
@relation = Post.order(id: :asc)
- @serach = {}
+ @search = {}
when "tags"
@relation = Tag.nonempty
@search = {}
diff --git a/app/views/static/not_found.html.erb b/app/views/static/not_found.html.erb
new file mode 100644
index 000000000..8393490a5
--- /dev/null
+++ b/app/views/static/not_found.html.erb
@@ -0,0 +1,26 @@
+<% page_title "Page not found" %>
+
+
+
+
+
Page not found
+
+ <% if @post.present? && @artist.present? %>
+
+ <%= link_to @post do %>
+ <%= tag.img src: @post.large_file_url %>
+ <% end %>
+
+
+ <%= link_to "post ##{@post.id}", @post %>
+ by <%= link_to @artist.name, posts_path(tags: @artist.name), class: tag_class(@artist) %>
+
+
+ <% else %>
+
Nobody here but us chickens!
+ <% end %>
+
+
<%= link_to "Return to previous page", :back %>
+
+
+
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index beed3096d..41fd594d9 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -281,6 +281,11 @@ module Danbooru
restricted_tags + %w[censored condom nipples nude penis pussy sexually_suggestive]
end
+ # If present, the 404 page will show a random post from this pool.
+ def page_not_found_pool_id
+ nil
+ end
+
# Tags that are only visible to Gold+ users.
def restricted_tags
[]
diff --git a/config/routes.rb b/config/routes.rb
index 94cd168a7..9ab7cacfa 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -321,5 +321,5 @@ Rails.application.routes.draw do
get "/mock/iqdbs/similar" => "mock_services#iqdbs_similar", as: "mock_iqdbs_similar"
post "/mock/iqdbs/similar" => "mock_services#iqdbs_similar"
- get "*other", :to => "static#not_found"
+ match "*other", to: "static#not_found", via: :all
end
diff --git a/public/404.html b/public/404.html
deleted file mode 100644
index 108e3337c..000000000
--- a/public/404.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Page not found
-
-
-
-
-
-
-
-
diff --git a/public/500.html b/public/500.html
deleted file mode 100644
index b6f61c973..000000000
--- a/public/500.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Failbooru
-
-
-
-
-
-
-
-
diff --git a/test/functional/static_controller_test.rb b/test/functional/static_controller_test.rb
index faca87b71..dff4ff680 100644
--- a/test/functional/static_controller_test.rb
+++ b/test/functional/static_controller_test.rb
@@ -47,10 +47,42 @@ class StaticControllerTest < ActionDispatch::IntegrationTest
end
context "not_found action" do
- should "work" do
+ should "return the 404 page for GET requests" do
get "/qwoiqogieqg"
assert_response 404
end
+
+ should "return the 404 page for POST requests" do
+ post "/qwoiqogieqg"
+ assert_response 404
+ end
+
+ should "return a JSON response for a 404'd JSON request" do
+ get "/qwoiqogieqg", as: :json
+
+ assert_response 404
+ assert_equal("Page not found", response.parsed_body["message"])
+ end
+
+ should "return an XML response for a 404'd XML request" do
+ get "/qwoiqogieqg", as: :xml
+
+ assert_response 404
+ assert_equal("Page not found", response.parsed_body.at("result").text)
+ end
+
+ should "render the 404 page when page_not_found_pool_id is configured" do
+ as(create(:user)) do
+ @post = create(:post, tag_string: "artist:bkub")
+ @pool = create(:pool, post_ids: [@post.id])
+ Danbooru.config.stubs(:page_not_found_pool_id).returns(@pool.id)
+ end
+
+ get "/qwoiqogieqg"
+
+ assert_response 404
+ assert_select "#c-static #a-not-found img", count: 1
+ end
end
context "bookmarklet action" do
From 7762489d7d9b4b7ed3408a989099dfb1d5ed84b9 Mon Sep 17 00:00:00 2001
From: evazion
Date: Wed, 23 Dec 2020 05:15:08 -0600
Subject: [PATCH 082/132] user upgrades: upgrade to new Stripe checkout system.
This upgrades from the legacy version of Stripe's checkout system to the
new version:
> The legacy version of Checkout presented customers with a modal dialog
> that collected card information, and returned a token or a source to
> your website. In contrast, the new version of Checkout is a smart
> payment page hosted by Stripe that creates payments or subscriptions. It
> supports Apple Pay, Dynamic 3D Secure, and many other features.
Basic overview of the new system:
* We send the user to a checkout page on Stripe.
* Stripe collects payment and sends us a webhook notification when the
order is complete.
* We receive the webhook notification and upgrade the user.
Docs:
* https://stripe.com/docs/payments/checkout
* https://stripe.com/docs/payments/checkout/migration#client-products
* https://stripe.com/docs/payments/handling-payment-events
* https://stripe.com/docs/payments/checkout/fulfill-orders
---
app/controllers/user_upgrades_controller.rb | 43 +----
app/controllers/webhooks_controller.rb | 13 ++
app/helpers/user_upgrades_helper.rb | 20 ---
app/logical/user_upgrade.rb | 138 ++++++++++++++-
.../user_upgrades/_stripe_payment.html.erb | 10 --
.../_stripe_payment_safebooru.html.erb | 3 -
app/views/user_upgrades/create.js.erb | 2 +
app/views/user_upgrades/new.html.erb | 22 ++-
config/danbooru_default_config.rb | 3 +
config/initializers/stripe.rb | 1 +
config/routes.rb | 3 +
.../checkout.session.completed.json | 55 ++++++
.../payment_intent.created.json | 67 +++++++
.../user_upgrades_controller_test.rb | 109 ++----------
test/functional/webhooks_controller_test.rb | 167 ++++++++++++++++++
test/test_helper.rb | 1 +
test/test_helpers/stripe_test_helper.rb | 13 ++
test/unit/user_upgrade_test.rb | 41 +++++
18 files changed, 536 insertions(+), 175 deletions(-)
create mode 100644 app/controllers/webhooks_controller.rb
delete mode 100644 app/views/user_upgrades/_stripe_payment.html.erb
delete mode 100644 app/views/user_upgrades/_stripe_payment_safebooru.html.erb
create mode 100644 app/views/user_upgrades/create.js.erb
create mode 100644 test/fixtures/stripe-webhooks/checkout.session.completed.json
create mode 100644 test/fixtures/stripe-webhooks/payment_intent.created.json
create mode 100644 test/functional/webhooks_controller_test.rb
create mode 100644 test/test_helpers/stripe_test_helper.rb
create mode 100644 test/unit/user_upgrade_test.rb
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index be7ee116a..e2b8729d6 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -1,11 +1,12 @@
class UserUpgradesController < ApplicationController
helper_method :user
- skip_before_action :verify_authenticity_token, only: [:create]
+ respond_to :js, :html
def create
- if params[:stripeToken]
- create_stripe
- end
+ @user_upgrade = UserUpgrade.new(recipient: user, purchaser: CurrentUser.user, level: params[:level].to_i)
+ @checkout = @user_upgrade.create_checkout
+
+ respond_with(@user_upgrade)
end
def new
@@ -22,38 +23,4 @@ class UserUpgradesController < ApplicationController
CurrentUser.user
end
end
-
- private
-
- def create_stripe
- @user = user
-
- if params[:desc] == "Upgrade to Gold"
- level = User::Levels::GOLD
- cost = UserUpgrade.gold_price
- elsif params[:desc] == "Upgrade to Platinum"
- level = User::Levels::PLATINUM
- cost = UserUpgrade.platinum_price
- elsif params[:desc] == "Upgrade Gold to Platinum" && @user.level == User::Levels::GOLD
- level = User::Levels::PLATINUM
- cost = UserUpgrade.upgrade_price
- else
- raise "Invalid desc"
- end
-
- begin
- charge = Stripe::Charge.create(amount: cost, currency: "usd", source: params[:stripeToken], description: params[:desc])
- @user.promote_to!(level, User.system, is_upgrade: true)
- flash[:success] = true
- rescue Stripe::StripeError => e
- DanbooruLogger.log(e)
- flash[:error] = e.message
- end
-
- if @user == CurrentUser.user
- redirect_to user_upgrade_path
- else
- redirect_to user_upgrade_path(user_id: params[:user_id])
- end
- end
end
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
new file mode 100644
index 000000000..0ff5cdeae
--- /dev/null
+++ b/app/controllers/webhooks_controller.rb
@@ -0,0 +1,13 @@
+class WebhooksController < ApplicationController
+ skip_forgery_protection only: :receive
+ rescue_with Stripe::SignatureVerificationError, status: 400
+
+ def receive
+ if params[:source] == "stripe"
+ UserUpgrade.receive_webhook(request)
+ head 200
+ else
+ head 400
+ end
+ end
+end
diff --git a/app/helpers/user_upgrades_helper.rb b/app/helpers/user_upgrades_helper.rb
index 0b7c301aa..8d37de45d 100644
--- a/app/helpers/user_upgrades_helper.rb
+++ b/app/helpers/user_upgrades_helper.rb
@@ -1,24 +1,4 @@
module UserUpgradesHelper
- def stripe_button(desc, cost, user)
- html = %{
-
- }
-
- raw(html)
- end
-
def cents_to_usd(cents)
number_to_currency(cents / 100, precision: 0)
end
diff --git a/app/logical/user_upgrade.rb b/app/logical/user_upgrade.rb
index b29909faf..f9af14708 100644
--- a/app/logical/user_upgrade.rb
+++ b/app/logical/user_upgrade.rb
@@ -1,13 +1,145 @@
class UserUpgrade
+ attr_reader :recipient, :purchaser, :level
+
+ def self.stripe_publishable_key
+ Danbooru.config.stripe_publishable_key
+ end
+
+ def self.stripe_webhook_secret
+ Danbooru.config.stripe_webhook_secret
+ end
+
def self.gold_price
2000
end
def self.platinum_price
- 4000
+ 2 * gold_price
end
- def self.upgrade_price
- 2000
+ def self.gold_to_platinum_price
+ platinum_price - gold_price
+ end
+
+ def initialize(recipient:, purchaser:, level:)
+ @recipient, @purchaser, @level = recipient, purchaser, level.to_i
+ end
+
+ def upgrade_type
+ if level == User::Levels::GOLD && recipient.level == User::Levels::MEMBER
+ :gold_upgrade
+ elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::MEMBER
+ :platinum_upgrade
+ elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::GOLD
+ :gold_to_platinum_upgrade
+ else
+ raise ArgumentError, "Invalid upgrade"
+ end
+ end
+
+ def upgrade_price
+ case upgrade_type
+ when :gold_upgrade
+ UserUpgrade.gold_price
+ when :platinum_upgrade
+ UserUpgrade.platinum_price
+ when :gold_to_platinum_upgrade
+ UserUpgrade.gold_to_platinum_price
+ end
+ end
+
+ def upgrade_description
+ case upgrade_type
+ when :gold_upgrade
+ "Upgrade to Gold"
+ when :platinum_upgrade
+ "Upgrade to Platinum"
+ when :gold_to_platinum_upgrade
+ "Upgrade Gold to Platinum"
+ end
+ end
+
+ def is_gift?
+ recipient != purchaser
+ end
+
+ def process_upgrade!
+ recipient.with_lock do
+ upgrade_recipient!
+ end
+ end
+
+ def upgrade_recipient!
+ recipient.promote_to!(level, User.system, is_upgrade: true)
+ end
+
+ concerning :StripeMethods do
+ def create_checkout
+ Stripe::Checkout::Session.create(
+ mode: "payment",
+ success_url: Routes.user_upgrade_url(user_id: recipient.id),
+ cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id),
+ client_reference_id: "user_#{purchaser.id}",
+ customer_email: recipient.email_address&.address,
+ payment_method_types: ["card"],
+ line_items: [{
+ price_data: {
+ unit_amount: upgrade_price,
+ currency: "usd",
+ product_data: {
+ name: upgrade_description,
+ },
+ },
+ quantity: 1,
+ }],
+ metadata: {
+ purchaser_id: purchaser.id,
+ recipient_id: recipient.id,
+ purchaser_name: purchaser.name,
+ recipient_name: recipient.name,
+ upgrade_type: upgrade_type,
+ is_gift: is_gift?,
+ level: level,
+ },
+ )
+ end
+
+ class_methods do
+ def register_webhook
+ webhook = Stripe::WebhookEndpoint.create({
+ url: Routes.webhook_user_upgrade_url(source: "stripe"),
+ enabled_events: [
+ "payment_intent.created",
+ "payment_intent.payment_failed",
+ "checkout.session.completed",
+ ],
+ })
+
+ webhook.secret
+ end
+
+ def receive_webhook(request)
+ event = build_event(request)
+
+ if event.type == "checkout.session.completed"
+ checkout_session_completed(event)
+ end
+ end
+
+ def build_event(request)
+ payload = request.body.read
+ signature = request.headers["Stripe-Signature"]
+ Stripe::Webhook.construct_event(payload, signature, stripe_webhook_secret)
+ end
+
+ def checkout_session_completed(event)
+ recipient = User.find(event.data.object.metadata.recipient_id)
+ purchaser = User.find(event.data.object.metadata.purchaser_id)
+ level = event.data.object.metadata.level
+
+ user_upgrade = UserUpgrade.new(recipient: recipient, purchaser: purchaser, level: level)
+ user_upgrade.process_upgrade!
+ end
+ end
end
end
diff --git a/app/views/user_upgrades/_stripe_payment.html.erb b/app/views/user_upgrades/_stripe_payment.html.erb
deleted file mode 100644
index b9e43052d..000000000
--- a/app/views/user_upgrades/_stripe_payment.html.erb
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
You can pay with a credit or debit card. Safebooru uses Stripe as a payment intermediary so none of your personal information will be stored on the site.
-
- <% if user.level < User::Levels::GOLD %>
- <%= stripe_button("Upgrade to Gold", UserUpgrade.gold_price, user) %>
- <%= stripe_button("Upgrade to Platinum", UserUpgrade.platinum_price, user) %>
- <% elsif user.level < User::Levels::PLATINUM %>
- <%= stripe_button("Upgrade Gold to Platinum", UserUpgrade.upgrade_price, user) %>
- <% end %>
-
diff --git a/app/views/user_upgrades/_stripe_payment_safebooru.html.erb b/app/views/user_upgrades/_stripe_payment_safebooru.html.erb
deleted file mode 100644
index 730eab9f6..000000000
--- a/app/views/user_upgrades/_stripe_payment_safebooru.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
You can pay with a credit or debit card on <%= link_to "Safebooru", new_user_upgrade_url(host: "safebooru.donmai.us", protocol: "https") %>. Your account will then also be upgraded on Danbooru. You can login to Safebooru with the same username and password you use on Danbooru.
-
diff --git a/app/views/user_upgrades/create.js.erb b/app/views/user_upgrades/create.js.erb
new file mode 100644
index 000000000..e08bc3438
--- /dev/null
+++ b/app/views/user_upgrades/create.js.erb
@@ -0,0 +1,2 @@
+var stripe = Stripe("<%= j UserUpgrade.stripe_publishable_key %>");
+stripe.redirectToCheckout({ sessionId: "<%= j @checkout.id %>" });
diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb
index 4cd610bb3..2ed284092 100644
--- a/app/views/user_upgrades/new.html.erb
+++ b/app/views/user_upgrades/new.html.erb
@@ -1,5 +1,6 @@
<% page_title "Account Upgrade" %>
-<% meta_description "Upgrade to a Gold or Platinum account on #{Danbooru.config.app_name}." %>
+<% meta_description "Upgrade to a Gold or Platinum account." %>
+
<%= render "users/secondary_links" %>
@@ -96,9 +97,24 @@
<% if CurrentUser.is_anonymous? %>
<%= link_to "Sign up", new_user_path %> or <%= link_to "login", login_path(url: new_user_upgrade_path) %> first to upgrade your account.
<% elsif CurrentUser.safe_mode? %>
- <%= render "stripe_payment" %>
+
+
You can pay with a credit or debit card. Safebooru uses Stripe
+ as a payment intermediary so none of your personal information will be stored on the site.
+
+ <% if user.level < User::Levels::GOLD %>
+
<%= button_to "Upgrade to Gold", user_upgrade_path(user_id: user.id, level: User::Levels::GOLD), remote: true, disable_with: "Redirecting..." %>
+
<%= button_to "Upgrade to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>
+ <% elsif user.level < User::Levels::PLATINUM %>
+
<%= button_to "Upgrade Gold to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>
+ <% end %>
+
<% else %>
- <%= render "stripe_payment_safebooru" %>
+
+
You can pay with a credit or debit card on
+ <%= link_to "Safebooru", new_user_upgrade_url(user_id: user.id, host: "safebooru.donmai.us", protocol: "https") %>.
+ Your account will then also be upgraded on Danbooru. You can login to
+ Safebooru with the same username and password you use on Danbooru.
+
<% end %>
<% end %>
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 41fd594d9..75622de73 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -359,6 +359,9 @@ module Danbooru
def stripe_publishable_key
end
+ def stripe_webhook_secret
+ end
+
def twitter_api_key
end
diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb
index 43495044b..7a60018c9 100644
--- a/config/initializers/stripe.rb
+++ b/config/initializers/stripe.rb
@@ -1 +1,2 @@
Stripe.api_key = Danbooru.config.stripe_secret_key
+Stripe.api_version = "2020-08-27"
diff --git a/config/routes.rb b/config/routes.rb
index 9ab7cacfa..b027bb3a3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -257,6 +257,9 @@ Rails.application.routes.draw do
resource :user_upgrade, :only => [:new, :create, :show]
resources :user_feedbacks, except: [:destroy]
resources :user_name_change_requests, only: [:new, :create, :show, :index]
+ resources :webhooks do
+ post :receive, on: :collection
+ end
resources :wiki_pages, id: /.+?(?=\.json|\.xml|\.html)|.+/ do
put :revert, on: :member
get :search, on: :collection
diff --git a/test/fixtures/stripe-webhooks/checkout.session.completed.json b/test/fixtures/stripe-webhooks/checkout.session.completed.json
new file mode 100644
index 000000000..975bb555d
--- /dev/null
+++ b/test/fixtures/stripe-webhooks/checkout.session.completed.json
@@ -0,0 +1,55 @@
+{
+ "id": "evt_000",
+ "object": "event",
+ "api_version": "2020-08-27",
+ "created": 1608705740,
+ "data": {
+ "object": {
+ "id": "cs_test_000",
+ "object": "checkout.session",
+ "allow_promotion_codes": null,
+ "amount_subtotal": 2000,
+ "amount_total": 2000,
+ "billing_address_collection": null,
+ "cancel_url": "http://localhost/user_upgrade/new",
+ "client_reference_id": "user_12345",
+ "currency": "usd",
+ "customer": "cus_000",
+ "customer_email": null,
+ "livemode": false,
+ "locale": null,
+ "metadata": {
+ "purchaser_id": "12345",
+ "recipient_id": "12345",
+ "purchaser_name": "user_12345",
+ "recipient_name": "user_12345",
+ "upgrade_type": "gold_upgrade",
+ "is_gift": "false",
+ "level": "30"
+ },
+ "mode": "payment",
+ "payment_intent": "pi_000",
+ "payment_method_types": [
+ "card"
+ ],
+ "payment_status": "paid",
+ "setup_intent": null,
+ "shipping": null,
+ "shipping_address_collection": null,
+ "submit_type": null,
+ "subscription": null,
+ "success_url": "http://localhost/user_upgrade?user_id=12345",
+ "total_details": {
+ "amount_discount": 0,
+ "amount_tax": 0
+ }
+ }
+ },
+ "livemode": false,
+ "pending_webhooks": 3,
+ "request": {
+ "id": null,
+ "idempotency_key": null
+ },
+ "type": "checkout.session.completed"
+}
diff --git a/test/fixtures/stripe-webhooks/payment_intent.created.json b/test/fixtures/stripe-webhooks/payment_intent.created.json
new file mode 100644
index 000000000..915aa9f90
--- /dev/null
+++ b/test/fixtures/stripe-webhooks/payment_intent.created.json
@@ -0,0 +1,67 @@
+{
+ "id": "evt_000",
+ "object": "event",
+ "api_version": "2020-08-27",
+ "created": 1608705945,
+ "data": {
+ "object": {
+ "id": "pi_000",
+ "object": "payment_intent",
+ "amount": 2000,
+ "amount_capturable": 0,
+ "amount_received": 0,
+ "application": null,
+ "application_fee_amount": null,
+ "canceled_at": null,
+ "cancellation_reason": null,
+ "capture_method": "automatic",
+ "charges": {
+ "object": "list",
+ "data": [],
+ "has_more": false,
+ "total_count": 0,
+ "url": "/v1/charges?payment_intent=pi_000"
+ },
+ "client_secret": "pi_000",
+ "confirmation_method": "automatic",
+ "created": 1608705945,
+ "currency": "usd",
+ "customer": null,
+ "description": null,
+ "invoice": null,
+ "last_payment_error": null,
+ "livemode": false,
+ "metadata": {},
+ "next_action": null,
+ "on_behalf_of": null,
+ "payment_method": null,
+ "payment_method_options": {
+ "card": {
+ "installments": null,
+ "network": null,
+ "request_three_d_secure": "automatic"
+ }
+ },
+ "payment_method_types": [
+ "card"
+ ],
+ "receipt_email": null,
+ "review": null,
+ "setup_future_usage": null,
+ "shipping": null,
+ "source": null,
+ "statement_descriptor": null,
+ "statement_descriptor_suffix": null,
+ "status": "requires_payment_method",
+ "transfer_data": null,
+ "transfer_group": null
+ }
+ },
+ "livemode": false,
+ "pending_webhooks": 3,
+ "request": {
+ "id": "req_000",
+ "idempotency_key": null
+ },
+ "type": "payment_intent.created"
+}
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index db88aa327..58e92a8aa 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -1,14 +1,6 @@
require 'test_helper'
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
- setup do
- StripeMock.start
- end
-
- teardown do
- StripeMock.stop
- end
-
context "The user upgrades controller" do
context "new action" do
should "render" do
@@ -25,103 +17,24 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
end
context "create action" do
- setup do
- @user = create(:user)
- @token = StripeMock.generate_card_token
- end
+ mock_stripe!
- context "a self upgrade" do
- should "upgrade a Member to Gold" do
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
-
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_gold?)
- end
-
- should "upgrade a Member to Platinum" do
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Platinum" }
-
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_platinum?)
- end
-
- should "upgrade a Gold user to Platinum" do
- @user.update!(level: User::Levels::GOLD)
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade Gold to Platinum" }
-
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_platinum?)
- end
-
- should "log an account upgrade modaction" do
- assert_difference("ModAction.user_account_upgrade.count") do
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
- end
- end
-
- should "send the user a dmail" do
- assert_difference("@user.dmails.received.count") do
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
- end
- end
- end
-
- context "a gifted upgrade" do
- should "upgrade the user to Gold" do
- @other_user = create(:user)
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @other_user.id }
-
- assert_redirected_to user_upgrade_path(user_id: @other_user.id)
- assert_equal(true, @other_user.reload.is_gold?)
- assert_equal(false, @user.reload.is_gold?)
- end
- end
-
- context "an upgrade for a user above Platinum level" do
- should "not demote the user" do
- @builder = create(:builder_user)
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @builder.id }
-
- assert_response 403
- assert_equal(true, @builder.reload.is_builder?)
- end
- end
-
- context "an upgrade with a missing Stripe token" do
- should "not upgrade the user" do
- post_auth user_upgrade_path, @user, params: { desc: "Upgrade to Gold" }
+ context "for a self upgrade to Gold" do
+ should "redirect the user to the Stripe checkout page" do
+ user = create(:member_user)
+ post_auth user_upgrade_path(user_id: user.id), user, params: { level: User::Levels::GOLD }, xhr: true
assert_response :success
- assert_equal(true, @user.reload.is_member?)
end
end
- context "an upgrade with an invalid Stripe token" do
- should "not upgrade the user" do
- post_auth user_upgrade_path, @user, params: { stripeToken: "garbage", desc: "Upgrade to Gold" }
+ context "for a gifted upgrade to Gold" do
+ should "redirect the user to the Stripe checkout page" do
+ recipient = create(:member_user)
+ purchaser = create(:member_user)
+ post_auth user_upgrade_path(user_id: recipient.id), purchaser, params: { level: User::Levels::GOLD }, xhr: true
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_member?)
- end
- end
-
- context "an upgrade with an credit card that is declined" do
- should "not upgrade the user" do
- StripeMock.prepare_card_error(:card_declined)
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
-
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_member?)
- end
- end
-
- context "an upgrade with an credit card that is expired" do
- should "not upgrade the user" do
- StripeMock.prepare_card_error(:expired_card)
- post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
-
- assert_redirected_to user_upgrade_path
- assert_equal(true, @user.reload.is_member?)
+ assert_response :success
end
end
end
diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb
new file mode 100644
index 000000000..afb596be4
--- /dev/null
+++ b/test/functional/webhooks_controller_test.rb
@@ -0,0 +1,167 @@
+require 'test_helper'
+
+class WebhooksControllerTest < ActionDispatch::IntegrationTest
+ mock_stripe!
+
+ def post_webhook(*args, **metadata)
+ event = StripeMock.mock_webhook_event(*args, metadata: metadata)
+ signature = generate_stripe_signature(event)
+ headers = { "Stripe-Signature": signature }
+
+ post receive_webhooks_path(source: "stripe"), headers: headers, params: event, as: :json
+ end
+
+ # https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues/467#issuecomment-634674913
+ # https://stripe.com/docs/webhooks/signatures
+ def generate_stripe_signature(event)
+ time = Time.now
+ secret = UserUpgrade.stripe_webhook_secret
+ signature = Stripe::Webhook::Signature.compute_signature(time, event.to_json, secret)
+ Stripe::Webhook::Signature.generate_header(time, signature, scheme: Stripe::Webhook::Signature::EXPECTED_SCHEME)
+ end
+
+ context "The webhooks controller" do
+ context "receive action" do
+ context "for a request from an unrecognized source" do
+ should "fail" do
+ post receive_webhooks_path(source: "blah")
+ assert_response 400
+ end
+ end
+
+ context "for a Stripe webhook" do
+ context "with a missing signature" do
+ should "fail" do
+ event = StripeMock.mock_webhook_event("payment_intent.created")
+ post receive_webhooks_path(source: "stripe"), params: event, as: :json
+
+ assert_response 400
+ end
+ end
+
+ context "with an invalid signature" do
+ should "fail" do
+ event = StripeMock.mock_webhook_event("payment_intent.created")
+ headers = { "Stripe-Signature": "blah" }
+ post receive_webhooks_path(source: "stripe"), headers: headers, params: event, as: :json
+
+ assert_response 400
+ end
+ end
+
+ context "for a payment_intent.created event" do
+ should "work" do
+ post_webhook("payment_intent.created")
+
+ assert_response 200
+ end
+ end
+
+ context "for a checkout.session.completed event" do
+ context "for a self upgrade" do
+ context "of a Member to Gold" do
+ should "upgrade the user" do
+ @user = create(:member_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @user.id,
+ purchaser_id: @user.id,
+ upgrade_type: "gold_upgrade",
+ level: User::Levels::GOLD,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::GOLD, @user.reload.level)
+ end
+ end
+
+ context "of a Member to Platinum" do
+ should "upgrade the user" do
+ @user = create(:member_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @user.id,
+ purchaser_id: @user.id,
+ upgrade_type: "platinum_upgrade",
+ level: User::Levels::PLATINUM,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::PLATINUM, @user.reload.level)
+ end
+ end
+
+ context "of a Gold user to Platinum" do
+ should "upgrade the user" do
+ @user = create(:gold_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @user.id,
+ purchaser_id: @user.id,
+ upgrade_type: "gold_to_platinum_upgrade",
+ level: User::Levels::PLATINUM,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::PLATINUM, @user.reload.level)
+ end
+ end
+ end
+
+ context "for a gifted upgrade" do
+ context "of a Member to Gold" do
+ should "upgrade the user" do
+ @recipient = create(:member_user)
+ @purchaser = create(:member_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @recipient.id,
+ purchaser_id: @purchaser.id,
+ upgrade_type: "gold_upgrade",
+ level: User::Levels::GOLD,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::GOLD, @recipient.reload.level)
+ end
+ end
+
+ context "of a Member to Platinum" do
+ should "upgrade the user" do
+ @recipient = create(:member_user)
+ @purchaser = create(:member_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @recipient.id,
+ purchaser_id: @purchaser.id,
+ upgrade_type: "platinum_upgrade",
+ level: User::Levels::PLATINUM,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
+ end
+ end
+
+ context "of a Gold user to Platinum" do
+ should "upgrade the user" do
+ @recipient = create(:gold_user)
+ @purchaser = create(:member_user)
+
+ post_webhook("checkout.session.completed", {
+ recipient_id: @recipient.id,
+ purchaser_id: @purchaser.id,
+ upgrade_type: "gold_to_platinum_upgrade",
+ level: User::Levels::PLATINUM,
+ })
+
+ assert_response 200
+ assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 1af778120..ca6a4b4f2 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -26,6 +26,7 @@ class ActiveSupport::TestCase
include DownloadTestHelper
include IqdbTestHelper
include UploadTestHelper
+ extend StripeTestHelper
mock_post_version_service!
mock_pool_version_service!
diff --git a/test/test_helpers/stripe_test_helper.rb b/test/test_helpers/stripe_test_helper.rb
new file mode 100644
index 000000000..d1f0f7126
--- /dev/null
+++ b/test/test_helpers/stripe_test_helper.rb
@@ -0,0 +1,13 @@
+StripeMock.webhook_fixture_path = "test/fixtures/stripe-webhooks"
+
+module StripeTestHelper
+ def mock_stripe!
+ setup do
+ StripeMock.start
+ end
+
+ teardown do
+ StripeMock.stop
+ end
+ end
+end
diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb
new file mode 100644
index 000000000..b6d228246
--- /dev/null
+++ b/test/unit/user_upgrade_test.rb
@@ -0,0 +1,41 @@
+require 'test_helper'
+
+class UserUpgradeTest < ActiveSupport::TestCase
+ context "UserUpgrade:" do
+ context "the #process_upgrade! method" do
+ setup do
+ @user = create(:user)
+ @user_upgrade = UserUpgrade.new(recipient: @user, purchaser: @user, level: User::Levels::GOLD)
+ end
+
+ should "update the user's level" do
+ @user_upgrade.process_upgrade!
+ assert_equal(User::Levels::GOLD, @user.reload.level)
+ end
+
+ should "log an account upgrade modaction" do
+ assert_difference("ModAction.user_account_upgrade.count") do
+ @user_upgrade.process_upgrade!
+ end
+ end
+
+ should "send the user a dmail" do
+ assert_difference("@user.dmails.received.count") do
+ @user_upgrade.process_upgrade!
+ end
+ end
+
+ context "for an upgrade for a user above Platinum level" do
+ should "not demote the user" do
+ @user.update!(level: User::Levels::BUILDER)
+
+ assert_raise(User::PrivilegeError) do
+ @user_upgrade.process_upgrade!
+ end
+
+ assert_equal(true, @user.reload.is_builder?)
+ end
+ end
+ end
+ end
+end
From 74ed2a8b966073cbc67677059a1cde1640929535 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 24 Dec 2020 06:40:20 -0600
Subject: [PATCH 083/132] user upgrades: add UserUpgrade model.
Add a model to store the status of user upgrades.
* Store the upgrade purchaser and the upgrade receiver (these are
different for a gifted upgrade, the same for a self upgrade).
* Store the upgrade type: gold, platinum, or gold-to-platinum upgrades.
* Store the upgrade status:
** pending: User is still on the Stripe checkout page, no payment
received yet.
** processing: User has completed checkout, but the checkout status in
Stripe is still 'unpaid'.
** complete: We've received notification from Stripe that the payment
has gone through and the user has been upgraded.
* Store the Stripe checkout ID, to cross-reference the upgrade record on
Danbooru with the checkout record on Stripe.
This is the upgrade flow:
* When the user clicks the upgrade button on the upgrade page, we call
POST /user_upgrades and create a pending UserUpgrade.
* We redirect the user to the checkout page on Stripe.
* When the user completes checkout on Stripe, Stripe sends us a webhook
notification at POST /webhooks/receive.
* When we receive the webhook, we check the payment status, and if it's
paid we mark the UserUpgrade as complete and upgrade the user.
* After Stripe sees that we have successfully processed the webhook,
they redirect the user to the /user_upgrades/:id page, where we show
the user their upgrade receipt.
---
app/controllers/user_upgrades_controller.rb | 7 +-
app/models/user.rb | 2 +
app/{logical => models}/user_upgrade.rb | 93 +++++----
app/policies/user_upgrade_policy.rb | 9 +
app/views/user_upgrades/new.html.erb | 10 +-
app/views/user_upgrades/show.html.erb | 47 +++--
config/routes.rb | 2 +-
.../20201224101208_create_user_upgrades.rb | 20 ++
db/structure.sql | 88 ++++++++-
test/factories/user_upgrade.rb | 38 ++++
.../user_upgrades_controller_test.rb | 176 ++++++++++++++++--
test/functional/webhooks_controller_test.rb | 108 +++++------
test/unit/user_upgrade_test.rb | 66 ++++---
13 files changed, 502 insertions(+), 164 deletions(-)
rename app/{logical => models}/user_upgrade.rb (61%)
create mode 100644 app/policies/user_upgrade_policy.rb
create mode 100644 db/migrate/20201224101208_create_user_upgrades.rb
create mode 100644 test/factories/user_upgrade.rb
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index e2b8729d6..4a0b199c2 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -3,8 +3,8 @@ class UserUpgradesController < ApplicationController
respond_to :js, :html
def create
- @user_upgrade = UserUpgrade.new(recipient: user, purchaser: CurrentUser.user, level: params[:level].to_i)
- @checkout = @user_upgrade.create_checkout
+ @user_upgrade = authorize UserUpgrade.create(recipient: user, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
+ @checkout = @user_upgrade.create_checkout!
respond_with(@user_upgrade)
end
@@ -13,7 +13,8 @@ class UserUpgradesController < ApplicationController
end
def show
- authorize User, :upgrade?
+ @user_upgrade = authorize UserUpgrade.find(params[:id])
+ respond_with(@user_upgrade)
end
def user
diff --git a/app/models/user.rb b/app/models/user.rb
index 071c42d04..01730294c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -99,6 +99,8 @@ class User < ApplicationRecord
has_many :post_votes
has_many :post_versions, foreign_key: :updater_id
has_many :bans, -> {order("bans.id desc")}
+ has_many :received_upgrades, class_name: "UserUpgrade", foreign_key: :recipient_id, dependent: :destroy
+ has_many :purchased_upgrades, class_name: "UserUpgrade", foreign_key: :purchaser_id, dependent: :destroy
has_one :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban"
has_one :api_key
diff --git a/app/logical/user_upgrade.rb b/app/models/user_upgrade.rb
similarity index 61%
rename from app/logical/user_upgrade.rb
rename to app/models/user_upgrade.rb
index f9af14708..3fdc81045 100644
--- a/app/logical/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -1,5 +1,18 @@
-class UserUpgrade
- attr_reader :recipient, :purchaser, :level
+class UserUpgrade < ApplicationRecord
+ belongs_to :recipient, class_name: "User"
+ belongs_to :purchaser, class_name: "User"
+
+ enum upgrade_type: {
+ gold: 0,
+ platinum: 10,
+ gold_to_platinum: 20
+ }, _suffix: "upgrade"
+
+ enum status: {
+ pending: 0,
+ processing: 10,
+ complete: 20
+ }
def self.stripe_publishable_key
Danbooru.config.stripe_publishable_key
@@ -21,51 +34,63 @@ class UserUpgrade
platinum_price - gold_price
end
- def initialize(recipient:, purchaser:, level:)
- @recipient, @purchaser, @level = recipient, purchaser, level.to_i
- end
-
- def upgrade_type
- if level == User::Levels::GOLD && recipient.level == User::Levels::MEMBER
- :gold_upgrade
- elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::MEMBER
- :platinum_upgrade
- elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::GOLD
- :gold_to_platinum_upgrade
+ def level
+ case upgrade_type
+ when "gold"
+ User::Levels::GOLD
+ when "platinum"
+ User::Levels::PLATINUM
+ when "gold_to_platinum"
+ User::Levels::PLATINUM
else
- raise ArgumentError, "Invalid upgrade"
+ raise NotImplementedError
end
end
def upgrade_price
case upgrade_type
- when :gold_upgrade
+ when "gold"
UserUpgrade.gold_price
- when :platinum_upgrade
+ when "platinum"
UserUpgrade.platinum_price
- when :gold_to_platinum_upgrade
+ when "gold_to_platinum"
UserUpgrade.gold_to_platinum_price
+ else
+ raise NotImplementedError
end
end
def upgrade_description
case upgrade_type
- when :gold_upgrade
+ when "gold"
"Upgrade to Gold"
- when :platinum_upgrade
+ when "platinum"
"Upgrade to Platinum"
- when :gold_to_platinum_upgrade
+ when "gold_to_platinum"
"Upgrade Gold to Platinum"
+ else
+ raise NotImplementedError
end
end
+ def level_string
+ User.level_string(level)
+ end
+
def is_gift?
recipient != purchaser
end
- def process_upgrade!
+ def process_upgrade!(payment_status)
recipient.with_lock do
- upgrade_recipient!
+ return if status == "complete"
+
+ if payment_status == "paid"
+ upgrade_recipient!
+ update!(status: :complete)
+ else
+ update!(status: :processing)
+ end
end
end
@@ -74,12 +99,12 @@ class UserUpgrade
end
concerning :StripeMethods do
- def create_checkout
- Stripe::Checkout::Session.create(
+ def create_checkout!
+ checkout = Stripe::Checkout::Session.create(
mode: "payment",
- success_url: Routes.user_upgrade_url(user_id: recipient.id),
+ success_url: Routes.user_upgrade_url(self),
cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id),
- client_reference_id: "user_#{purchaser.id}",
+ client_reference_id: "user_upgrade_#{id}",
customer_email: recipient.email_address&.address,
payment_method_types: ["card"],
line_items: [{
@@ -93,6 +118,7 @@ class UserUpgrade
quantity: 1,
}],
metadata: {
+ user_upgrade_id: id,
purchaser_id: purchaser.id,
recipient_id: recipient.id,
purchaser_name: purchaser.name,
@@ -102,6 +128,9 @@ class UserUpgrade
level: level,
},
)
+
+ update!(stripe_id: checkout.id)
+ checkout
end
class_methods do
@@ -122,7 +151,7 @@ class UserUpgrade
event = build_event(request)
if event.type == "checkout.session.completed"
- checkout_session_completed(event)
+ checkout_session_completed(event.data.object)
end
end
@@ -132,13 +161,9 @@ class UserUpgrade
Stripe::Webhook.construct_event(payload, signature, stripe_webhook_secret)
end
- def checkout_session_completed(event)
- recipient = User.find(event.data.object.metadata.recipient_id)
- purchaser = User.find(event.data.object.metadata.purchaser_id)
- level = event.data.object.metadata.level
-
- user_upgrade = UserUpgrade.new(recipient: recipient, purchaser: purchaser, level: level)
- user_upgrade.process_upgrade!
+ def checkout_session_completed(checkout)
+ user_upgrade = UserUpgrade.find(checkout.metadata.user_upgrade_id)
+ user_upgrade.process_upgrade!(checkout.payment_status)
end
end
end
diff --git a/app/policies/user_upgrade_policy.rb b/app/policies/user_upgrade_policy.rb
new file mode 100644
index 000000000..78365557f
--- /dev/null
+++ b/app/policies/user_upgrade_policy.rb
@@ -0,0 +1,9 @@
+class UserUpgradePolicy < ApplicationPolicy
+ def create?
+ user.is_member?
+ end
+
+ def show?
+ record.recipient == user || record.purchaser == user || user.is_owner?
+ end
+end
diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb
index 2ed284092..89d7ea4d6 100644
--- a/app/views/user_upgrades/new.html.erb
+++ b/app/views/user_upgrades/new.html.erb
@@ -101,11 +101,11 @@
You can pay with a credit or debit card. Safebooru uses Stripe
as a payment intermediary so none of your personal information will be stored on the site.
- <% if user.level < User::Levels::GOLD %>
- <%= button_to "Upgrade to Gold", user_upgrade_path(user_id: user.id, level: User::Levels::GOLD), remote: true, disable_with: "Redirecting..." %>
- <%= button_to "Upgrade to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>
- <% elsif user.level < User::Levels::PLATINUM %>
- <%= button_to "Upgrade Gold to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>
+ <% if user.level == User::Levels::MEMBER %>
+ <%= button_to "Upgrade to Gold", user_upgrades_path(user_id: user.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %>
+ <%= button_to "Upgrade to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %>
+ <% elsif user.level == User::Levels::GOLD %>
+ <%= button_to "Upgrade Gold to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %>
<% end %>
<% else %>
diff --git a/app/views/user_upgrades/show.html.erb b/app/views/user_upgrades/show.html.erb
index 014421551..7eeddffdf 100644
--- a/app/views/user_upgrades/show.html.erb
+++ b/app/views/user_upgrades/show.html.erb
@@ -1,22 +1,47 @@
-<% page_title "Account Upgraded" %>
+<% page_title "User Upgrade Status" %>
<%= render "users/secondary_links" %>
- <% if flash[:success] %>
-
Congratulations!
+
User Upgrade
- <% if user != CurrentUser.user %>
-
<%= user.name %> is now a <%= user.level_string %> user. Thanks for supporting the site!
+
+
+
+ Purchased
+ <%= time_ago_in_words_tagged @user_upgrade.updated_at %>
+ by <%= link_to_user @user_upgrade.purchaser %>
+ <% if @user_upgrade.is_gift? %>
+ for <%= link_to_user @user_upgrade.recipient %>
+ <% end %>
+
+
+ Upgrade Type
+ <%= @user_upgrade.upgrade_type.humanize %>
+
+
+ Status
+ <%= @user_upgrade.status.humanize %>
+
+
+
+
+ <% if @user_upgrade.status == "complete" %>
+ <% if @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.recipient %>
+
<%= link_to_user @user_upgrade.purchaser %> has upgraded your account to <%= @user_upgrade.level_string %>. Enjoy your new account!
+ <% elsif @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.purchaser %>
+
<%= link_to_user @user_upgrade.recipient %> is now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!
<% else %>
-
You are now a <%= user.level_string %> user. Thanks for supporting the site!
+
You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!
<% end %>
-
<%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to start using your new account.
- <% elsif flash[:error] %>
-
An error occurred!
-
<%= flash[:error] %>
-
<%= link_to "Try again", new_user_upgrade_path %>
+
<%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to continue using the site.
+ <% else %>
+ <%= content_for :html_header do %>
+
+ <% end %>
+
+
This order is still being processed. You will be notified as soon as the order is complete.
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index b027bb3a3..4bceb91fe 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -254,7 +254,7 @@ Rails.application.routes.draw do
get :custom_style
end
end
- resource :user_upgrade, :only => [:new, :create, :show]
+ resources :user_upgrades, only: [:new, :create, :show]
resources :user_feedbacks, except: [:destroy]
resources :user_name_change_requests, only: [:new, :create, :show, :index]
resources :webhooks do
diff --git a/db/migrate/20201224101208_create_user_upgrades.rb b/db/migrate/20201224101208_create_user_upgrades.rb
new file mode 100644
index 000000000..46b44f22a
--- /dev/null
+++ b/db/migrate/20201224101208_create_user_upgrades.rb
@@ -0,0 +1,20 @@
+class CreateUserUpgrades < ActiveRecord::Migration[6.1]
+ def change
+ create_table :user_upgrades do |t|
+ t.timestamps
+
+ t.references :recipient, index: true, null: false
+ t.references :purchaser, index: true, null: false
+ t.integer :upgrade_type, index: true, null: false
+ t.integer :status, index: true, null: false
+ t.string :stripe_id, index: true, null: true
+ end
+
+ # Reserve ID space for backfilling old upgrades.
+ reversible do |dir|
+ dir.up do
+ execute "SELECT setval('user_upgrades_id_seq', 25000, false)"
+ end
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 5f8a9b7da..f57281afa 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -3104,6 +3104,41 @@ CREATE SEQUENCE public.user_name_change_requests_id_seq
ALTER SEQUENCE public.user_name_change_requests_id_seq OWNED BY public.user_name_change_requests.id;
+--
+-- Name: user_upgrades; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.user_upgrades (
+ id bigint NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL,
+ recipient_id bigint NOT NULL,
+ purchaser_id bigint NOT NULL,
+ upgrade_type integer NOT NULL,
+ status integer NOT NULL,
+ stripe_id character varying
+);
+
+
+--
+-- Name: user_upgrades_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.user_upgrades_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: user_upgrades_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.user_upgrades_id_seq OWNED BY public.user_upgrades.id;
+
+
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@@ -4172,6 +4207,13 @@ ALTER TABLE ONLY public.user_feedback ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.user_name_change_requests ALTER COLUMN id SET DEFAULT nextval('public.user_name_change_requests_id_seq'::regclass);
+--
+-- Name: user_upgrades id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.user_upgrades ALTER COLUMN id SET DEFAULT nextval('public.user_upgrades_id_seq'::regclass);
+
+
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4537,6 +4579,14 @@ ALTER TABLE ONLY public.user_name_change_requests
ADD CONSTRAINT user_name_change_requests_pkey PRIMARY KEY (id);
+--
+-- Name: user_upgrades user_upgrades_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.user_upgrades
+ ADD CONSTRAINT user_upgrades_pkey PRIMARY KEY (id);
+
+
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -7045,6 +7095,41 @@ CREATE INDEX index_user_name_change_requests_on_original_name ON public.user_nam
CREATE INDEX index_user_name_change_requests_on_user_id ON public.user_name_change_requests USING btree (user_id);
+--
+-- Name: index_user_upgrades_on_purchaser_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_user_upgrades_on_purchaser_id ON public.user_upgrades USING btree (purchaser_id);
+
+
+--
+-- Name: index_user_upgrades_on_recipient_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_user_upgrades_on_recipient_id ON public.user_upgrades USING btree (recipient_id);
+
+
+--
+-- Name: index_user_upgrades_on_status; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_user_upgrades_on_status ON public.user_upgrades USING btree (status);
+
+
+--
+-- Name: index_user_upgrades_on_stripe_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_user_upgrades_on_stripe_id ON public.user_upgrades USING btree (stripe_id);
+
+
+--
+-- Name: index_user_upgrades_on_upgrade_type; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_user_upgrades_on_upgrade_type ON public.user_upgrades USING btree (upgrade_type);
+
+
--
-- Name: index_users_on_created_at; Type: INDEX; Schema: public; Owner: -
--
@@ -7437,6 +7522,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200816175151'),
('20201201211748'),
('20201213052805'),
-('20201219201007');
+('20201219201007'),
+('20201224101208');
diff --git a/test/factories/user_upgrade.rb b/test/factories/user_upgrade.rb
new file mode 100644
index 000000000..a3b13c4fd
--- /dev/null
+++ b/test/factories/user_upgrade.rb
@@ -0,0 +1,38 @@
+FactoryBot.define do
+ factory(:user_upgrade) do
+ recipient { create(:member_user) }
+ purchaser { recipient }
+ upgrade_type { "gold" }
+ status { "pending" }
+ stripe_id { nil }
+
+ factory(:self_gold_upgrade) do
+ upgrade_type { "gold" }
+ end
+
+ factory(:self_platinum_upgrade) do
+ upgrade_type { "platinum" }
+ end
+
+ factory(:self_gold_to_platinum_upgrade) do
+ recipient { create(:gold_user) }
+ upgrade_type { "gold_to_platinum" }
+ end
+
+ factory(:gift_gold_upgrade) do
+ purchaser { create(:user) }
+ upgrade_type { "gold" }
+ end
+
+ factory(:gift_platinum_upgrade) do
+ purchaser { create(:user) }
+ upgrade_type { "platinum" }
+ end
+
+ factory(:gift_gold_to_platinum_upgrade) do
+ recipient { create(:gold_user) }
+ purchaser { create(:user) }
+ upgrade_type { "gold_to_platinum" }
+ end
+ end
+end
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index 58e92a8aa..ba6240730 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -3,38 +3,184 @@ require 'test_helper'
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
context "The user upgrades controller" do
context "new action" do
- should "render" do
+ should "render for a self upgrade" do
+ @user = create(:user)
+ get_auth new_user_upgrade_path, @user
+
+ assert_response :success
+ end
+
+ should "render for a gift upgrade" do
+ @recipient = create(:user)
+ get_auth new_user_upgrade_path(user_id: @recipient.id), create(:user)
+
+ assert_response :success
+ end
+
+ should "render for an anonymous user" do
get new_user_upgrade_path
+
assert_response :success
end
end
context "show action" do
- should "render" do
- get_auth user_upgrade_path, create(:user)
- assert_response :success
+ context "for a completed upgrade" do
+ should "render for a self upgrade" do
+ @user_upgrade = create(:self_gold_upgrade, status: "complete")
+ get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response :success
+ end
+
+ should "render for a gift upgrade for the purchaser" do
+ @user_upgrade = create(:gift_gold_upgrade, status: "complete")
+ get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response :success
+ end
+
+ should "render for a gift upgrade for the recipient" do
+ @user_upgrade = create(:gift_gold_upgrade, status: "complete")
+ get_auth user_upgrade_path(@user_upgrade), @user_upgrade.recipient
+
+ assert_response :success
+ end
+
+ should "render for the site owner" do
+ @user_upgrade = create(:self_gold_upgrade, status: "complete")
+ get_auth user_upgrade_path(@user_upgrade), create(:owner_user)
+
+ assert_response :success
+ end
+
+ should "be inaccessible to other users" do
+ @user_upgrade = create(:self_gold_upgrade, status: "complete")
+ get_auth user_upgrade_path(@user_upgrade), create(:user)
+
+ assert_response 403
+ end
+ end
+
+ context "for a pending upgrade" do
+ should "render" do
+ @user_upgrade = create(:self_gold_upgrade, status: "pending")
+ get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response :success
+ end
end
end
context "create action" do
mock_stripe!
- context "for a self upgrade to Gold" do
- should "redirect the user to the Stripe checkout page" do
- user = create(:member_user)
- post_auth user_upgrade_path(user_id: user.id), user, params: { level: User::Levels::GOLD }, xhr: true
+ context "for a self upgrade" do
+ context "to Gold" do
+ should "create a pending upgrade" do
+ @user = create(:member_user)
- assert_response :success
+ post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "gold" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @user.purchased_upgrades.last
+ assert_equal(@user, @user_upgrade.purchaser)
+ assert_equal(@user, @user_upgrade.recipient)
+ assert_equal("gold", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
+ end
+
+ context "to Platinum" do
+ should "create a pending upgrade" do
+ @user = create(:member_user)
+
+ post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "platinum" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @user.purchased_upgrades.last
+ assert_equal(@user, @user_upgrade.purchaser)
+ assert_equal(@user, @user_upgrade.recipient)
+ assert_equal("platinum", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
+ end
+
+ context "from Gold to Platinum" do
+ should "create a pending upgrade" do
+ @user = create(:member_user)
+
+ post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "gold_to_platinum" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @user.purchased_upgrades.last
+ assert_equal(@user, @user_upgrade.purchaser)
+ assert_equal(@user, @user_upgrade.recipient)
+ assert_equal("gold_to_platinum", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
end
end
- context "for a gifted upgrade to Gold" do
- should "redirect the user to the Stripe checkout page" do
- recipient = create(:member_user)
- purchaser = create(:member_user)
- post_auth user_upgrade_path(user_id: recipient.id), purchaser, params: { level: User::Levels::GOLD }, xhr: true
+ context "for a gifted upgrade" do
+ context "to Gold" do
+ should "create a pending upgrade" do
+ @recipient = create(:member_user)
+ @purchaser = create(:member_user)
- assert_response :success
+ post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "gold" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @purchaser.purchased_upgrades.last
+ assert_equal(@purchaser, @user_upgrade.purchaser)
+ assert_equal(@recipient, @user_upgrade.recipient)
+ assert_equal("gold", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
+ end
+
+ context "to Platinum" do
+ should "create a pending upgrade" do
+ @recipient = create(:member_user)
+ @purchaser = create(:member_user)
+
+ post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "platinum" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @purchaser.purchased_upgrades.last
+ assert_equal(@purchaser, @user_upgrade.purchaser)
+ assert_equal(@recipient, @user_upgrade.recipient)
+ assert_equal("platinum", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
+ end
+
+ context "from Gold to Platinum" do
+ should "create a pending upgrade" do
+ @recipient = create(:gold_user)
+ @purchaser = create(:member_user)
+
+ post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "gold_to_platinum" }, xhr: true
+ assert_response :success
+
+ @user_upgrade = @purchaser.purchased_upgrades.last
+ assert_equal(@purchaser, @user_upgrade.purchaser)
+ assert_equal(@recipient, @user_upgrade.recipient)
+ assert_equal("gold_to_platinum", @user_upgrade.upgrade_type)
+ assert_equal("pending", @user_upgrade.status)
+ assert_not_nil(@user_upgrade.stripe_id)
+ assert_match(/redirectToCheckout/, response.body)
+ end
end
end
end
diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb
index afb596be4..dd10f8df8 100644
--- a/test/functional/webhooks_controller_test.rb
+++ b/test/functional/webhooks_controller_test.rb
@@ -3,8 +3,8 @@ require 'test_helper'
class WebhooksControllerTest < ActionDispatch::IntegrationTest
mock_stripe!
- def post_webhook(*args, **metadata)
- event = StripeMock.mock_webhook_event(*args, metadata: metadata)
+ def post_webhook(*args, payment_status: "paid", **metadata)
+ event = StripeMock.mock_webhook_event(*args, payment_status: payment_status, metadata: metadata)
signature = generate_stripe_signature(event)
headers = { "Stripe-Signature": signature }
@@ -58,105 +58,83 @@ class WebhooksControllerTest < ActionDispatch::IntegrationTest
end
context "for a checkout.session.completed event" do
+ context "for completed event with an unpaid payment status" do
+ should "not upgrade the user" do
+ @user_upgrade = create(:self_gold_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id, payment_status: "unpaid" })
+
+ assert_response 200
+ assert_equal("processing", @user_upgrade.reload.status)
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.reload.level)
+ end
+ end
+
context "for a self upgrade" do
- context "of a Member to Gold" do
+ context "to Gold" do
should "upgrade the user" do
- @user = create(:member_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @user.id,
- purchaser_id: @user.id,
- upgrade_type: "gold_upgrade",
- level: User::Levels::GOLD,
- })
+ @user_upgrade = create(:self_gold_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::GOLD, @user.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level)
end
end
- context "of a Member to Platinum" do
+ context "to Platinum" do
should "upgrade the user" do
- @user = create(:member_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @user.id,
- purchaser_id: @user.id,
- upgrade_type: "platinum_upgrade",
- level: User::Levels::PLATINUM,
- })
+ @user_upgrade = create(:self_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::PLATINUM, @user.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
end
end
- context "of a Gold user to Platinum" do
+ context "from Gold to Platinum" do
should "upgrade the user" do
- @user = create(:gold_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @user.id,
- purchaser_id: @user.id,
- upgrade_type: "gold_to_platinum_upgrade",
- level: User::Levels::PLATINUM,
- })
+ @user_upgrade = create(:self_gold_to_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::PLATINUM, @user.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
end
end
end
context "for a gifted upgrade" do
- context "of a Member to Gold" do
+ context "to Gold" do
should "upgrade the user" do
- @recipient = create(:member_user)
- @purchaser = create(:member_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @recipient.id,
- purchaser_id: @purchaser.id,
- upgrade_type: "gold_upgrade",
- level: User::Levels::GOLD,
- })
+ @user_upgrade = create(:gift_gold_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::GOLD, @recipient.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level)
end
end
- context "of a Member to Platinum" do
+ context "to Platinum" do
should "upgrade the user" do
- @recipient = create(:member_user)
- @purchaser = create(:member_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @recipient.id,
- purchaser_id: @purchaser.id,
- upgrade_type: "platinum_upgrade",
- level: User::Levels::PLATINUM,
- })
+ @user_upgrade = create(:gift_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
end
end
- context "of a Gold user to Platinum" do
+ context "from Gold to Platinum" do
should "upgrade the user" do
- @recipient = create(:gold_user)
- @purchaser = create(:member_user)
-
- post_webhook("checkout.session.completed", {
- recipient_id: @recipient.id,
- purchaser_id: @purchaser.id,
- upgrade_type: "gold_to_platinum_upgrade",
- level: User::Levels::PLATINUM,
- })
+ @user_upgrade = create(:gift_gold_to_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
assert_response 200
- assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
end
end
end
diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb
index b6d228246..070fa2f48 100644
--- a/test/unit/user_upgrade_test.rb
+++ b/test/unit/user_upgrade_test.rb
@@ -3,37 +3,45 @@ require 'test_helper'
class UserUpgradeTest < ActiveSupport::TestCase
context "UserUpgrade:" do
context "the #process_upgrade! method" do
- setup do
- @user = create(:user)
- @user_upgrade = UserUpgrade.new(recipient: @user, purchaser: @user, level: User::Levels::GOLD)
- end
-
- should "update the user's level" do
- @user_upgrade.process_upgrade!
- assert_equal(User::Levels::GOLD, @user.reload.level)
- end
-
- should "log an account upgrade modaction" do
- assert_difference("ModAction.user_account_upgrade.count") do
- @user_upgrade.process_upgrade!
- end
- end
-
- should "send the user a dmail" do
- assert_difference("@user.dmails.received.count") do
- @user_upgrade.process_upgrade!
- end
- end
-
- context "for an upgrade for a user above Platinum level" do
- should "not demote the user" do
- @user.update!(level: User::Levels::BUILDER)
-
- assert_raise(User::PrivilegeError) do
- @user_upgrade.process_upgrade!
+ context "for a self upgrade" do
+ context "to Gold" do
+ setup do
+ @user_upgrade = create(:self_gold_upgrade)
end
- assert_equal(true, @user.reload.is_builder?)
+ should "update the user's level if the payment status is paid" do
+ @user_upgrade.process_upgrade!("paid")
+
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.level)
+ assert_equal("complete", @user_upgrade.status)
+ end
+
+ should "not update the user's level if the payment is unpaid" do
+ @user_upgrade.process_upgrade!("unpaid")
+
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level)
+ assert_equal("processing", @user_upgrade.status)
+ end
+
+ should "not update the user's level if the upgrade status is complete" do
+ @user_upgrade.update!(status: "complete")
+ @user_upgrade.process_upgrade!("paid")
+
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level)
+ assert_equal("complete", @user_upgrade.status)
+ end
+
+ should "log an account upgrade modaction" do
+ assert_difference("ModAction.user_account_upgrade.count") do
+ @user_upgrade.process_upgrade!("paid")
+ end
+ end
+
+ should "send the recipient a dmail" do
+ assert_difference("@user_upgrade.recipient.dmails.received.count") do
+ @user_upgrade.process_upgrade!("paid")
+ end
+ end
end
end
end
From 058d71aa30f00ec0d13223174d76bc70b1273807 Mon Sep 17 00:00:00 2001
From: evazion
Date: Thu, 24 Dec 2020 20:32:20 -0600
Subject: [PATCH 084/132] user upgrades: send dmail to purchaser for gifted
upgrades.
* Refactor to move upgrade logic from UserPromotion to UserUpgrade.
* Send the recipient and the purchaser of a gifted upgrade separate
dmail notifications.
---
app/logical/user_promotion.rb | 12 +++------
app/models/user_upgrade.rb | 51 +++++++++++++++++++++++++++--------
db/populate.rb | 4 +--
3 files changed, 46 insertions(+), 21 deletions(-)
diff --git a/app/logical/user_promotion.rb b/app/logical/user_promotion.rb
index 6420db5e3..2ee8ee685 100644
--- a/app/logical/user_promotion.rb
+++ b/app/logical/user_promotion.rb
@@ -1,13 +1,12 @@
class UserPromotion
- attr_reader :user, :promoter, :new_level, :old_can_approve_posts, :old_can_upload_free, :can_upload_free, :can_approve_posts, :is_upgrade
+ attr_reader :user, :promoter, :new_level, :old_can_approve_posts, :old_can_upload_free, :can_upload_free, :can_approve_posts
- def initialize(user, promoter, new_level, can_upload_free: nil, can_approve_posts: nil, is_upgrade: false)
+ def initialize(user, promoter, new_level, can_upload_free: nil, can_approve_posts: nil)
@user = user
@promoter = promoter
@new_level = new_level.to_i
@can_upload_free = can_upload_free
@can_approve_posts = can_approve_posts
- @is_upgrade = is_upgrade
end
def promote!
@@ -21,7 +20,7 @@ class UserPromotion
user.can_approve_posts = can_approve_posts unless can_approve_posts.nil?
user.inviter = promoter
- create_user_feedback unless is_upgrade
+ create_user_feedback
create_dmail
create_mod_actions
@@ -40,8 +39,7 @@ class UserPromotion
end
if user.level_changed?
- category = is_upgrade ? :user_account_upgrade : :user_level_change
- ModAction.log(%{"#{user.name}":#{Routes.user_path(user)} level changed #{user.level_string_was} -> #{user.level_string}}, category, promoter)
+ ModAction.log(%{"#{user.name}":#{Routes.user_path(user)} level changed #{user.level_string_was} -> #{user.level_string}}, :user_level_change, promoter)
end
end
@@ -54,8 +52,6 @@ class UserPromotion
raise User::PrivilegeError, "You can't promote other users to your rank or above"
elsif user.level >= promoter.level
raise User::PrivilegeError, "You can't promote or demote other users at your rank or above"
- elsif is_upgrade && user.is_builder?
- raise User::PrivilegeError, "You can't upgrade a user that is above Platinum level"
end
end
diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb
index 3fdc81045..5863d94a8 100644
--- a/app/models/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -81,21 +81,50 @@ class UserUpgrade < ApplicationRecord
recipient != purchaser
end
- def process_upgrade!(payment_status)
- recipient.with_lock do
- return if status == "complete"
+ concerning :UpgradeMethods do
+ def process_upgrade!(payment_status)
+ recipient.with_lock do
+ return if status == "complete"
- if payment_status == "paid"
- upgrade_recipient!
- update!(status: :complete)
- else
- update!(status: :processing)
+ if payment_status == "paid"
+ upgrade_recipient!
+ create_mod_action!
+ dmail_recipient!
+ dmail_purchaser!
+ update!(status: :complete)
+ else
+ update!(status: :processing)
+ end
end
end
- end
- def upgrade_recipient!
- recipient.promote_to!(level, User.system, is_upgrade: true)
+ def upgrade_recipient!
+ recipient.update!(level: level, inviter: User.system)
+ end
+
+ def create_mod_action!
+ ModAction.log(%{"#{recipient.name}":#{Routes.user_path(recipient)} level changed #{User.level_string(recipient.level_before_last_save)} -> #{recipient.level_string}}, :user_account_upgrade, purchaser)
+ end
+
+ def dmail_recipient!
+ if is_gift?
+ body = "Congratulations, your account has been upgraded to #{level_string} by <@#{purchaser.name}>. Enjoy!"
+ else
+ body = "You are now a #{level_string} user. Thanks for supporting #{Danbooru.config.canonical_app_name}!"
+ end
+
+ title = "You have been upgraded to #{level_string}!"
+ Dmail.create_automated(to: recipient, title: title, body: body)
+ end
+
+ def dmail_purchaser!
+ return unless is_gift?
+
+ title = "#{recipient.name} has been upgraded to #{level_string}!"
+ body = "<@#{recipient.name}> is now a #{level_string} user. Thanks for supporting #{Danbooru.config.canonical_app_name}!"
+
+ Dmail.create_automated(to: purchaser, title: title, body: body)
+ end
end
concerning :StripeMethods do
diff --git a/db/populate.rb b/db/populate.rb
index 7a66a7080..76a517ed3 100644
--- a/db/populate.rb
+++ b/db/populate.rb
@@ -68,14 +68,14 @@ if User.count == 0
:password => "password1",
:password_confirmation => "password1"
)
- newuser.promote_to!(User::Levels::BUILDER, :can_upload_free => true, :is_upgrade => true, :skip_dmail => true)
+ newuser.promote_to!(User::Levels::BUILDER, :can_upload_free => true, :skip_dmail => true)
newuser = User.create(
:name => "approver",
:password => "password1",
:password_confirmation => "password1"
)
- newuser.promote_to!(User::Levels::BUILDER, :can_approve_posts => true, :is_upgrade => true, :skip_dmail => true)
+ newuser.promote_to!(User::Levels::BUILDER, :can_approve_posts => true, :skip_dmail => true)
end
0.upto(10) do |i|
From 069231a33ba9232730f3975f1ae5314451286c29 Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 00:27:08 -0600
Subject: [PATCH 085/132] user upgrades: update upgrade landing page.
* Add a frequently asked questions section.
* Add nicer looking upgrade buttons.
* Format the page nicer.
* Prevent users from attempting invalid upgrades on users that are
already Platinum or above.
---
app/controllers/user_upgrades_controller.rb | 11 +-
app/javascript/src/styles/base/020_base.scss | 11 +
app/javascript/src/styles/base/040_colors.css | 18 +-
.../src/styles/specific/user_upgrades.scss | 83 ++++--
app/models/user_upgrade.rb | 8 +
app/policies/user_upgrade_policy.rb | 4 +
app/views/user_upgrades/new.html.erb | 261 +++++++++++-------
app/views/users/_statistics.html.erb | 9 +-
.../user_upgrades_controller_test.rb | 25 +-
9 files changed, 283 insertions(+), 147 deletions(-)
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index 4a0b199c2..62e4cee10 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -1,15 +1,18 @@
class UserUpgradesController < ApplicationController
- helper_method :user
respond_to :js, :html
def create
- @user_upgrade = authorize UserUpgrade.create(recipient: user, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
+ @user_upgrade = authorize UserUpgrade.create(recipient: recipient, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
@checkout = @user_upgrade.create_checkout!
respond_with(@user_upgrade)
end
def new
+ @user_upgrade = authorize UserUpgrade.new(recipient: recipient, purchaser: CurrentUser.user)
+ @recipient = @user_upgrade.recipient
+
+ respond_with(@user_upgrade)
end
def show
@@ -17,7 +20,9 @@ class UserUpgradesController < ApplicationController
respond_with(@user_upgrade)
end
- def user
+ private
+
+ def recipient
if params[:user_id]
User.find(params[:user_id])
else
diff --git a/app/javascript/src/styles/base/020_base.scss b/app/javascript/src/styles/base/020_base.scss
index 00180c2c6..3230c5db2 100644
--- a/app/javascript/src/styles/base/020_base.scss
+++ b/app/javascript/src/styles/base/020_base.scss
@@ -118,6 +118,17 @@ table tfoot {
margin-top: 2em;
}
+details {
+ border-bottom: 1px solid var(--details-border);
+
+ summary {
+ cursor: pointer;
+ user-select: none;
+ outline: none;
+ line-height: 2em;
+ }
+}
+
.fineprint {
color: var(--muted-text-color);
font-style: italic;
diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css
index 5dc30e914..4cf8a227e 100644
--- a/app/javascript/src/styles/base/040_colors.css
+++ b/app/javascript/src/styles/base/040_colors.css
@@ -35,9 +35,12 @@
--quick-search-form-background: var(--body-background-color);
+ --user-upgrade-basic-background-color: #F5F5FF;
--user-upgrade-gold-background-color: #FFF380;
--user-upgrade-platinum-background-color: #EEE;
- --user-upgrade-table-row-hover-background-color: #FEF;
+ --user-upgrade-button-text-color: white;
+ --user-upgrade-button-background-color: var(--link-color);
+ --user-upgrade-button-hover-background-color: hsl(213, 100%, 40%);
--table-header-border: 2px solid #666;
--table-row-border: 1px solid #CCC;
@@ -72,6 +75,8 @@
--comment-sticky-background-color: var(--subnav-menu-background-color);
+ --details-border: #DDD;
+
--post-tooltip-background-color: var(--body-background-color);
--post-tooltip-border-color: hsla(210, 100%, 3%, 0.15);
--post-tooltip-box-shadow: 0 4px 14px -2px hsla(210, 100%, 3%, 0.10);
@@ -157,7 +162,8 @@
--bulk-update-request-failed-color: red;
--login-link-color: #E00;
- --footer-border: 1px solid #EEE;
+ --footer-border: 1px solid #DDD;
+ --details-border: #DDD;
--jquery-ui-widget-content-background: var(--body-background-color);
--jquery-ui-widget-content-text-color: var(--text-color);
@@ -255,6 +261,7 @@ body[data-current-user-theme="dark"] {
--subnav-menu-background-color: var(--grey-2);
--responsive-menu-background-color: var(--grey-3);
--footer-border: 1px solid var(--grey-3);
+ --details-border: var(--grey-3);
--table-header-border: 2px solid var(--grey-3);
--table-even-row-background: var(--grey-2);
@@ -419,9 +426,12 @@ body[data-current-user-theme="dark"] {
--uploads-dropzone-progress-bar-foreground-color: var(--link-color);
--uploads-dropzone-progress-bar-background-color: var(--link-hover-color);
- --user-upgrade-gold-background-color: var(--yellow-0);
+ --user-upgrade-basic-background-color: var(--grey-2);
+ --user-upgrade-gold-background-color: var(--indigo-0);
--user-upgrade-platinum-background-color: var(--blue-0);
- --user-upgrade-table-row-hover-background-color: transparent;
+ --user-upgrade-button-text-color: white;
+ --user-upgrade-button-background-color: var(--link-color);
+ --user-upgrade-button-hover-background-color: var(--link-hover-color);
--wiki-page-other-name-background-color: var(--grey-3);
--wiki-page-versions-diff-del-background: var(--red-0);
diff --git a/app/javascript/src/styles/specific/user_upgrades.scss b/app/javascript/src/styles/specific/user_upgrades.scss
index 2ab6b4625..b32fdf483 100644
--- a/app/javascript/src/styles/specific/user_upgrades.scss
+++ b/app/javascript/src/styles/specific/user_upgrades.scss
@@ -1,43 +1,66 @@
div#c-user-upgrades {
div#a-new {
- form.stripe {
- display: inline;
- }
+ margin: 0 auto;
- div.section {
- margin-bottom: 2em;
- }
-
- div#feature-comparison {
- overflow: hidden;
+ * {
margin-bottom: 1em;
+ }
- table {
- width: 100%;
+ h1 {
+ text-align: center;
+ }
- colgroup {
- width: 10em;
- }
+ .login-button, form.button_to input[type="submit"] {
+ display: inline-block;
- colgroup#gold {
- background-color: var(--user-upgrade-gold-background-color);
- }
+ color: var(--user-upgrade-button-text-color);
+ background-color: var(--user-upgrade-button-background-color);
- colgroup#platinum {
- background-color: var(--user-upgrade-platinum-background-color);
- }
+ border: none;
+ border-radius: 4px;
+ padding: 0.75em;
- td, th {
- text-align: center;
- vertical-align: top;
- padding: 0.5em 0;
- }
+ transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+ box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12);
- tbody {
- tr:hover {
- background-color: var(--user-upgrade-table-row-hover-background-color);
- }
- }
+ &:hover:not([disabled]) {
+ background-color: var(--user-upgrade-button-hover-background-color);
+ box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)
+ }
+
+ &[disabled] {
+ background-color: grey;
+ cursor: default;
+ }
+ }
+
+ table#feature-comparison {
+ width: 100%;
+
+ th {
+ font-weight: bold;
+ }
+
+ colgroup {
+ width: 10em;
+ }
+
+ colgroup#basic {
+ background-color: var(--user-upgrade-basic-background-color);
+ }
+
+ colgroup#gold {
+ background-color: var(--user-upgrade-gold-background-color);
+ }
+
+ colgroup#platinum {
+ background-color: var(--user-upgrade-platinum-background-color);
+ }
+
+ td, th {
+ text-align: center;
+ vertical-align: top;
+ padding: 0.5em 0;
}
}
}
diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb
index 5863d94a8..41aa69c6e 100644
--- a/app/models/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -14,6 +14,14 @@ class UserUpgrade < ApplicationRecord
complete: 20
}
+ def self.enabled?
+ stripe_secret_key.present? && stripe_publishable_key.present? && stripe_webhook_secret.present?
+ end
+
+ def self.stripe_secret_key
+ Danbooru.config.stripe_secret_key
+ end
+
def self.stripe_publishable_key
Danbooru.config.stripe_publishable_key
end
diff --git a/app/policies/user_upgrade_policy.rb b/app/policies/user_upgrade_policy.rb
index 78365557f..a6568f5db 100644
--- a/app/policies/user_upgrade_policy.rb
+++ b/app/policies/user_upgrade_policy.rb
@@ -3,6 +3,10 @@ class UserUpgradePolicy < ApplicationPolicy
user.is_member?
end
+ def new?
+ UserUpgrade.enabled?
+ end
+
def show?
record.recipient == user || record.purchaser == user || user.is_owner?
end
diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb
index 89d7ea4d6..b5d278fd9 100644
--- a/app/views/user_upgrades/new.html.erb
+++ b/app/views/user_upgrades/new.html.erb
@@ -6,116 +6,165 @@
-
Upgrade Account
- <% unless params[:user_id] %>
-
Want more searching power? Upgrade your account and become a power user of the best database of anime artwork on the internet.
+ <% if @user_upgrade.is_gift? %>
+
Gift Account Upgrade
-
-
-
-
-
- Basic
- Gold
- Platinum
-
-
-
-
-
-
-
-
- Cost
- Free
-
- <%= cents_to_usd(UserUpgrade.gold_price) %>
- One time fee
-
-
- <%= cents_to_usd(UserUpgrade.platinum_price) %>
- One time fee
-
-
-
- Tag Limit
- 2
- <%= Danbooru.config.base_tag_query_limit %>
- <%= Danbooru.config.base_tag_query_limit*2 %>
-
-
- Favorite Limit
- 10,000
- 20,000
- Unlimited
-
-
- Favorite Groups
- 3
- 5
- 10
-
-
- Page Limit
- 1,000
- 2,000
- 5,000
-
-
- Saved Searches
- 250
- 250
- 1,000
-
-
- See Hidden Tags
- No
- Yes
- Yes
-
-
- Search Timeout
- 3 sec
- 6 sec
- 9 sec
-
-
-
-
+ <% if @user_upgrade.recipient.is_platinum? %>
+
<%= link_to_user @recipient %> is already above Platinum level and can't be upgraded!
+ <% else %>
+
You are gifting this upgrade to <%= link_to_user @user_upgrade.recipient %>.
+ <% end %>
+ <% else %>
+
Upgrade Account
+
+
Upgrading your account gives you exclusive benefits and helps support
+ <%= Danbooru.config.canonical_app_name %>. Your support helps keep the
+ site ad-free for everyone!
+
+
You can also gift an account upgrade to someone else. Just go to
+ their profile page and look for a "Gift Upgrade" link.
<% end %>
-
- <% if params[:user_id] %>
-
You are gifting this account upgrade to <%= link_to user.pretty_name, user_path(params[:user_id]) %>.
- <% else %>
-
You can also upgrade someone else's account for the same price. The easiest way is to go to their profile page and look for a "Gift Upgrade" link.
- <% end %>
-
-
- <% if Danbooru.config.stripe_publishable_key %>
- <% if CurrentUser.is_anonymous? %>
-
<%= link_to "Sign up", new_user_path %> or <%= link_to "login", login_path(url: new_user_upgrade_path) %> first to upgrade your account.
- <% elsif CurrentUser.safe_mode? %>
-
-
You can pay with a credit or debit card. Safebooru uses Stripe
- as a payment intermediary so none of your personal information will be stored on the site.
-
- <% if user.level == User::Levels::MEMBER %>
-
<%= button_to "Upgrade to Gold", user_upgrades_path(user_id: user.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %>
-
<%= button_to "Upgrade to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %>
- <% elsif user.level == User::Levels::GOLD %>
-
<%= button_to "Upgrade Gold to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %>
+
+
+
+
+ Basic
+ Gold
+ Platinum
+
+
+
+
+
+
+
+
+
+ Free
+
+ <%= cents_to_usd(UserUpgrade.gold_price) %>
+ One time fee
+
+
+ <%= cents_to_usd(UserUpgrade.platinum_price) %>
+ One time fee
+
+
+
+ Tag Limit
+ 2
+ <%= Danbooru.config.base_tag_query_limit %>
+ <%= Danbooru.config.base_tag_query_limit*2 %>
+
+
+ See Hidden Tags
+ No
+ Yes
+ Yes
+
+
+ Page Limit
+ 1,000
+ 2,000
+ 5,000
+
+
+ Favorite Limit
+ 10,000
+ 20,000
+ Unlimited
+
+
+ Favorite Groups
+ 3
+ 5
+ 10
+
+
+ Saved Searches
+ 250
+ 250
+ 1,000
+
+
+ Search Timeout
+ 3 sec
+ 6 sec
+ 9 sec
+
+
+
+ <% if @user_upgrade.purchaser.is_anonymous? %>
+ <%= link_to "Login", login_path(url: new_user_upgrade_path), class: "login-button" %>
+ <%= link_to "Get #{Danbooru.config.canonical_app_name} Gold", login_path(url: new_user_upgrade_path), class: "login-button" %>
+ <%= link_to "Get #{Danbooru.config.canonical_app_name} Platinum", login_path(url: new_user_upgrade_path), class: "login-button" %>
+ <% elsif @recipient.level == User::Levels::MEMBER %>
+
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %>
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %>
+ <% elsif @recipient.level == User::Levels::GOLD %>
+
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %>
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %>
+ <% else %>
+
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %>
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", nil, disabled: true %>
<% end %>
-
- <% else %>
-
-
You can pay with a credit or debit card on
- <%= link_to "Safebooru", new_user_upgrade_url(user_id: user.id, host: "safebooru.donmai.us", protocol: "https") %>.
- Your account will then also be upgraded on Danbooru. You can login to
- Safebooru with the same username and password you use on Danbooru.
-
- <% end %>
- <% end %>
+
+
+
+
+
Frequently Asked Questions
+
+
+
+ What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?
+
+ <%= Danbooru.config.canonical_app_name %> Gold lets you do more
+ complicated searches, and it lets you see hidden tags that non-Gold users
+ can't see. You can search more tags at once, browser deeper in search
+ results, and also keep more favorites, favorite groups, and saved searches.
+
+
+
+ What are the benefits of <%= Danbooru.config.canonical_app_name %> Platinum?
+
+ Platinum is like Gold, but it lets you search even more tags at once,
+ and keep even more favorites, favorite groups, and saved searches.
+
+
+
+ What payment methods do you support?
+
+ We support all major credit and debit cards, including international
+ cards. Payments are securely handled by Stripe .
+ We don't support PayPal or Bitcoin at this time.
+
+
+
+ Is this a subscription?
+
+ No, this is not a subscription. This is a one-time payment. You pay
+ only once and keep the upgrade forever.
+
+
+
+ If I upgrade to Gold first, can I upgrade to Platinum later?
+
+ Yes, if you have a Gold account, you can always upgrade to a Platinum
+ account later. You don't have to pay full price to upgrade from Gold to
+ Platinum. You only have to pay the difference.
+
+
+
+ What is your refund policy?
+
+ You can <%= link_to "contact us", contact_path %> to request a refund
+ for any reason within 48 hours of your purchase.
+
+
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb
index fe708c18c..581028cbd 100644
--- a/app/views/users/_statistics.html.erb
+++ b/app/views/users/_statistics.html.erb
@@ -75,8 +75,13 @@
Level
<%= user.level_string %>
- <% if CurrentUser.user == user && !CurrentUser.is_gold? %>
- (<%= link_to "upgrade", new_user_upgrade_path %>)
+
+ <% if !user.is_platinum? %>
+ <% if CurrentUser.user == user %>
+ (<%= link_to "Upgrade account", new_user_upgrade_path %>)
+ <% else %>
+ (<%= link_to "Gift upgrade", new_user_upgrade_path(user_id: user.id) %>)
+ <% end %>
<% end %>
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index ba6240730..4f466c86e 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -3,20 +3,41 @@ require 'test_helper'
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
context "The user upgrades controller" do
context "new action" do
- should "render for a self upgrade" do
+ should "render for a self upgrade to Gold" do
@user = create(:user)
get_auth new_user_upgrade_path, @user
assert_response :success
end
- should "render for a gift upgrade" do
+ should "render for a self upgrade to Platinum" do
+ @user = create(:gold_user)
+ get_auth new_user_upgrade_path, @user
+
+ assert_response :success
+ end
+
+ should "render for a gifted upgrade to Gold" do
@recipient = create(:user)
get_auth new_user_upgrade_path(user_id: @recipient.id), create(:user)
assert_response :success
end
+ should "render for a gifted upgrade to Platinum" do
+ @recipient = create(:gold_user)
+ get_auth new_user_upgrade_path(user_id: @recipient.id), create(:user)
+
+ assert_response :success
+ end
+
+ should "render for an invalid gifted upgrade to a user who is already Platinum" do
+ @recipient = create(:platinum_user)
+ get_auth new_user_upgrade_path(user_id: @recipient.id), create(:user)
+
+ assert_response :success
+ end
+
should "render for an anonymous user" do
get new_user_upgrade_path
From 96f08b78c5b47dface6660b1b20b1c9c68e54628 Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 00:37:15 -0600
Subject: [PATCH 086/132] /contact: update contact page with more contact
methods.
---
app/helpers/application_helper.rb | 5 +++--
app/views/static/contact.html.erb | 9 ++++++---
test/functional/static_controller_test.rb | 2 ++
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index b33c763d1..57f4819c0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -189,7 +189,7 @@ module ApplicationHelper
to_sentence(links, **options)
end
- def link_to_user(user)
+ def link_to_user(user, text = nil)
return "anonymous" if user.blank?
user_class = "user user-#{user.level_string.downcase}"
@@ -197,8 +197,9 @@ module ApplicationHelper
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(user.pretty_name, user_path(user), class: user_class, data: data)
+ link_to(text, user, class: user_class, data: data)
end
def mod_link_to_user(user, positive_or_negative)
diff --git a/app/views/static/contact.html.erb b/app/views/static/contact.html.erb
index edefb96a5..95b3a17c6 100644
--- a/app/views/static/contact.html.erb
+++ b/app/views/static/contact.html.erb
@@ -4,8 +4,11 @@
diff --git a/test/functional/static_controller_test.rb b/test/functional/static_controller_test.rb
index dff4ff680..13b99b53c 100644
--- a/test/functional/static_controller_test.rb
+++ b/test/functional/static_controller_test.rb
@@ -94,6 +94,8 @@ class StaticControllerTest < ActionDispatch::IntegrationTest
context "contact action" do
should "work" do
+ create(:owner_user)
+
get contact_path
assert_response :success
end
From 2d50ba6fd56ad4664a4a9a1d31926192bbeec91f Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 00:59:48 -0600
Subject: [PATCH 087/132] posts: fix /posts/random route.
Fixup for 039ccfa3a.
---
config/routes.rb | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/config/routes.rb b/config/routes.rb
index 4bceb91fe..1f39cbf94 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,5 +1,8 @@
Rails.application.routes.draw do
- resources :posts, only: [:index, :show, :update, :destroy]
+ resources :posts, only: [:index, :show, :update, :destroy] do
+ get :random, on: :collection
+ end
+
resources :autocomplete, only: [:index]
# XXX This comes *after* defining posts above because otherwise the paginator
@@ -187,9 +190,6 @@ Rails.application.routes.draw do
member { put :revert }
end
resource :votes, controller: "post_votes", only: [:create, :destroy], as: "post_votes"
- collection do
- get :random
- end
member do
put :revert
put :copy_notes
From e030a07816fec47c002eb7fc21edfb7bd3a0f62f Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 01:21:54 -0600
Subject: [PATCH 088/132] user upgrades: add index action.
---
app/controllers/user_upgrades_controller.rb | 7 ++++
app/models/user_upgrade.rb | 24 ++++++++++++
app/views/user_upgrades/index.html.erb | 38 +++++++++++++++++++
config/routes.rb | 2 +-
.../user_upgrades_controller_test.rb | 28 ++++++++++++++
5 files changed, 98 insertions(+), 1 deletion(-)
create mode 100644 app/views/user_upgrades/index.html.erb
diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index 62e4cee10..80c8fa303 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -15,6 +15,13 @@ class UserUpgradesController < ApplicationController
respond_with(@user_upgrade)
end
+ def index
+ @user_upgrades = authorize UserUpgrade.visible(CurrentUser.user).paginated_search(params, count_pages: true)
+ @user_upgrades = @user_upgrades.includes(:recipient, :purchaser) if request.format.html?
+
+ respond_with(@user_upgrades)
+ end
+
def show
@user_upgrade = authorize UserUpgrade.find(params[:id])
respond_with(@user_upgrade)
diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb
index 41aa69c6e..acb2eb32b 100644
--- a/app/models/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -14,6 +14,9 @@ class UserUpgrade < ApplicationRecord
complete: 20
}
+ scope :gifted, -> { where("recipient_id != purchaser_id") }
+ scope :self_upgrade, -> { where("recipient_id = purchaser_id") }
+
def self.enabled?
stripe_secret_key.present? && stripe_publishable_key.present? && stripe_webhook_secret.present?
end
@@ -89,6 +92,27 @@ class UserUpgrade < ApplicationRecord
recipient != purchaser
end
+ def self.visible(user)
+ if user.is_owner?
+ all
+ else
+ where(recipient: user).or(where(purchaser: user))
+ end
+ end
+
+ def self.search(params)
+ q = search_attributes(params, :id, :created_at, :updated_at, :upgrade_type, :status, :stripe_id, :recipient, :purchaser)
+
+ if params[:is_gifted].to_s.truthy?
+ q = q.gifted
+ elsif params[:is_gifted].to_s.falsy?
+ q = q.self_upgrade
+ end
+
+ q = q.apply_default_order(params)
+ q
+ end
+
concerning :UpgradeMethods do
def process_upgrade!(payment_status)
recipient.with_lock do
diff --git a/app/views/user_upgrades/index.html.erb b/app/views/user_upgrades/index.html.erb
new file mode 100644
index 000000000..611ea071e
--- /dev/null
+++ b/app/views/user_upgrades/index.html.erb
@@ -0,0 +1,38 @@
+
+
+ <%= search_form_for(user_upgrades_path) do |f| %>
+ <%= f.input :recipient_name, label: "Recipient", input_html: { value: params[:search][:recipient_name], data: { autocomplete: "user" } } %>
+ <%= f.input :purchaser_name, label: "Purchaser", input_html: { value: params[:search][:purchaser_name], data: { autocomplete: "user" } } %>
+ <%= f.input :upgrade_type, collection: UserUpgrade.upgrade_types, include_blank: true, selected: params[:search][:upgrade_type] %>
+ <%= f.input :status, collection: UserUpgrade.statuses, include_blank: true, selected: params[:search][:status] %>
+ <%= f.input :is_gifted, label: "Gifted?", as: :select, include_blank: true, selected: params[:search][:is_gifted] %>
+ <%= f.submit "Search" %>
+ <% end %>
+
+ <%= table_for @user_upgrades, class: "striped autofit" do |t| %>
+ <% t.column "Recipient" do |user_upgrade| %>
+ <%= link_to_user user_upgrade.recipient %>
+ <% end %>
+
+ <% t.column "Purchaser" do |user_upgrade| %>
+ <%= link_to_user user_upgrade.purchaser %>
+ <% end %>
+
+ <% t.column :upgrade_type do |user_upgrade| %>
+ <%= user_upgrade.upgrade_type.humanize %>
+ <% end %>
+
+ <% t.column "Gifted?" do |user_upgrade| %>
+ <%= "Yes" if user_upgrade.is_gift? %>
+ <% end %>
+
+ <% t.column :status %>
+
+ <% t.column "Updated" do |artist| %>
+ <%= time_ago_in_words_tagged(artist.updated_at) %>
+ <% end %>
+ <% end %>
+
+ <%= numbered_paginator(@user_upgrades) %>
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 1f39cbf94..a20f927f7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -254,7 +254,7 @@ Rails.application.routes.draw do
get :custom_style
end
end
- resources :user_upgrades, only: [:new, :create, :show]
+ resources :user_upgrades, only: [:new, :create, :show, :index]
resources :user_feedbacks, except: [:destroy]
resources :user_name_change_requests, only: [:new, :create, :show, :index]
resources :webhooks do
diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index 4f466c86e..75ff4f2f1 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -45,6 +45,34 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
end
end
+ context "index action" do
+ setup do
+ @self_upgrade = create(:self_gold_upgrade)
+ @gift_upgrade = create(:gift_gold_upgrade)
+ end
+
+ should "show the purchaser's upgrades to the purchaser" do
+ get_auth user_upgrades_path, @gift_upgrade.purchaser
+
+ assert_response :success
+ assert_select "#user-upgrade-#{@gift_upgrade.id}", count: 1
+ end
+
+ should "show the recipient's upgrades to the recipient" do
+ get_auth user_upgrades_path, @gift_upgrade.recipient
+
+ assert_response :success
+ assert_select "#user-upgrade-#{@gift_upgrade.id}", count: 1
+ end
+
+ should "not show upgrades to unrelated users" do
+ get_auth user_upgrades_path, create(:user)
+
+ assert_response :success
+ assert_select "#user-upgrade-#{@gift_upgrade.id}", count: 0
+ end
+ end
+
context "show action" do
context "for a completed upgrade" do
should "render for a self upgrade" do
From d9db32640afb4352733cd52edc10f2ba615eea70 Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 02:01:42 -0600
Subject: [PATCH 089/132] user upgrades: fix checkout form leaking recipient's
email.
The checkout form should be prefilled with the purchaser's email
address, not the recipient's.
---
app/models/user_upgrade.rb | 2 +-
test/unit/user_upgrade_test.rb | 14 ++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb
index acb2eb32b..55c388a46 100644
--- a/app/models/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -166,7 +166,7 @@ class UserUpgrade < ApplicationRecord
success_url: Routes.user_upgrade_url(self),
cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id),
client_reference_id: "user_upgrade_#{id}",
- customer_email: recipient.email_address&.address,
+ customer_email: purchaser.email_address&.address,
payment_method_types: ["card"],
line_items: [{
price_data: {
diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb
index 070fa2f48..93666aebb 100644
--- a/test/unit/user_upgrade_test.rb
+++ b/test/unit/user_upgrade_test.rb
@@ -45,5 +45,19 @@ class UserUpgradeTest < ActiveSupport::TestCase
end
end
end
+
+ context "the #create_checkout! method" do
+ context "for a gifted upgrade" do
+ context "to Gold" do
+ should "prefill the Stripe checkout page with the purchaser's email address" do
+ @user = create(:user, email_address: build(:email_address))
+ @user_upgrade = create(:gift_gold_upgrade, purchaser: @user)
+ @checkout = @user_upgrade.create_checkout!
+
+ assert_equal(@user.email_address.address, @checkout.customer_email)
+ end
+ end
+ end
+ end
end
end
From ae49ed2b1af0e798d7118205867dbe2397f5993b Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 02:43:56 -0600
Subject: [PATCH 090/132] api: fix legacy /post/index and /tag/index endpoints.
Fixup for a1cd9d2b5. The route order matters here, the legacy endpoints
need to go first.
---
config/routes.rb | 12 +++++------
test/functional/legacy_controller_test.rb | 25 +++++++++++++++++++++++
2 files changed, 31 insertions(+), 6 deletions(-)
create mode 100644 test/functional/legacy_controller_test.rb
diff --git a/config/routes.rb b/config/routes.rb
index a20f927f7..1ec90392d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -271,6 +271,12 @@ Rails.application.routes.draw do
end
end
+ # Legacy Danbooru 1 API endpoints
+ get "/tag/index.xml", :controller => "legacy", :action => "tags", :format => "xml"
+ get "/tag/index.json", :controller => "legacy", :action => "tags", :format => "json"
+ get "/post/index.xml", :controller => "legacy", :action => "posts", :format => "xml"
+ get "/post/index.json", :controller => "legacy", :action => "posts", :format => "json"
+
# Legacy Danbooru 1 redirects.
get "/artist" => redirect {|params, req| "/artists?page=#{req.params[:page]}&search[name]=#{CGI.escape(req.params[:name].to_s)}"}
get "/artist/show/:id" => redirect("/artists/%{id}")
@@ -294,12 +300,6 @@ Rails.application.routes.draw do
get "/wiki/show" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape(req.params[:title].to_s)}"}
get "/help/:title" => redirect {|params, req| "/wiki_pages?title=#{CGI.escape('help:' + req.params[:title])}"}
- # Legacy Danbooru 1 API endpoints
- get "/tag/index.xml", :controller => "legacy", :action => "tags", :format => "xml"
- get "/tag/index.json", :controller => "legacy", :action => "tags", :format => "json"
- get "/post/index.xml", :controller => "legacy", :action => "posts", :format => "xml"
- get "/post/index.json", :controller => "legacy", :action => "posts", :format => "json"
-
get "/login", to: "sessions#new", as: :login
get "/logout", to: "sessions#sign_out", as: :logout
get "/profile", to: "users#profile", as: :profile
diff --git a/test/functional/legacy_controller_test.rb b/test/functional/legacy_controller_test.rb
new file mode 100644
index 000000000..c4b591b70
--- /dev/null
+++ b/test/functional/legacy_controller_test.rb
@@ -0,0 +1,25 @@
+require 'test_helper'
+
+class LegacyControllerTest < ActionDispatch::IntegrationTest
+ context "The legacy controller" do
+ context "post action" do
+ should "work" do
+ get "/post/index.xml"
+ assert_response :success
+
+ get "/post/index.json"
+ assert_response :success
+ end
+ end
+
+ context "tag action" do
+ should "work" do
+ get "/tag/index.xml"
+ assert_response :success
+
+ get "/tag/index.json"
+ assert_response :success
+ end
+ end
+ end
+end
From fd182913829e65960df7c8a8633e25950914137c Mon Sep 17 00:00:00 2001
From: evazion
Date: Fri, 25 Dec 2020 05:46:04 -0600
Subject: [PATCH 091/132] Add Danbooru Winter Sale.
---
app/javascript/src/javascripts/common.js | 7 +++++++
app/models/user.rb | 4 +++-
app/models/user_upgrade.rb | 6 +++++-
app/views/layouts/default.html.erb | 19 +++++++++++++++++++
app/views/user_upgrades/new.html.erb | 10 ++++++++++
config/danbooru_default_config.rb | 6 ++++++
public/images/ablobgift.gif | Bin 0 -> 210166 bytes
public/images/blobgift.png | Bin 0 -> 15830 bytes
public/images/kemogift.png | Bin 0 -> 22398 bytes
public/images/padoru.gif | Bin 0 -> 22445 bytes
public/images/provgift.png | Bin 0 -> 16795 bytes
11 files changed, 50 insertions(+), 2 deletions(-)
create mode 100644 public/images/ablobgift.gif
create mode 100644 public/images/blobgift.png
create mode 100644 public/images/kemogift.png
create mode 100644 public/images/padoru.gif
create mode 100644 public/images/provgift.png
diff --git a/app/javascript/src/javascripts/common.js b/app/javascript/src/javascripts/common.js
index 04e2af233..94b6bb8d0 100644
--- a/app/javascript/src/javascripts/common.js
+++ b/app/javascript/src/javascripts/common.js
@@ -7,6 +7,13 @@ $(function() {
e.preventDefault();
});
+ $("#hide-promotion-notice").on("click.danbooru", function(e) {
+ $("#promotion-notice").hide();
+ Cookie.put("hide_promotion_notice", "1", 1);
+ Cookie.put("hide_upgrade_account_notice", "1", 1);
+ e.preventDefault();
+ });
+
$("#hide-dmail-notice").on("click.danbooru", function(e) {
var $dmail_notice = $("#dmail-notice");
$dmail_notice.hide();
diff --git a/app/models/user.rb b/app/models/user.rb
index 01730294c..732bf3079 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -404,7 +404,9 @@ class User < ApplicationRecord
end
def tag_query_limit
- if is_platinum?
+ if is_member? && Danbooru.config.is_promotion?
+ 1_000_000
+ elsif is_platinum?
Danbooru.config.base_tag_query_limit * 2
elsif is_gold?
Danbooru.config.base_tag_query_limit
diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb
index 55c388a46..7ccd87987 100644
--- a/app/models/user_upgrade.rb
+++ b/app/models/user_upgrade.rb
@@ -34,7 +34,11 @@ class UserUpgrade < ApplicationRecord
end
def self.gold_price
- 2000
+ if Danbooru.config.is_promotion?
+ 1500
+ else
+ 2000
+ end
end
def self.platinum_price
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index c79fa8acb..778a3c143 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -70,6 +70,25 @@
<%= render "users/verification_notice" %>
+ <% if Danbooru.config.is_promotion? && cookies[:hide_promotion_notice].blank? %>
+
+ <% if rand <= 0.0025 %>
+ <%= tag.img src: "/images/provgift.png", width: 24, height: 24 %>
+ <% elsif rand <= 0.0025 %>
+ <%= tag.img src: "/images/kemogift.png", width: 24, height: 24 %>
+ <% elsif rand <= 0.0025 %>
+ <%= tag.img src: "/images/padoru.gif", width: 24, height: 24 %>
+ <% elsif rand <= 0.0025 %>
+ <%= tag.img src: "/images/ablobgift.gif", width: 24, height: 24 %>
+ <% else %>
+ <%= tag.img src: "/images/blobgift.png", width: 24, height: 24 %>
+ <% end %>
+
+ <%= link_to "Danbooru Winter Sale!", forum_topic_path(17832) %> 25% off Gold and unlimited searches for Members!
+ (<%= link_to "hide", "#", id: "hide-promotion-notice" %>)
+
+ <% end %>
+
<% if !CurrentUser.is_anonymous? && !CurrentUser.is_gold? && cookies[:hide_upgrade_account_notice].blank? && params[:action] != "upgrade_information" %>
<%= render "users/upgrade_notice" %>
<% end %>
diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb
index b5d278fd9..6f125ebed 100644
--- a/app/views/user_upgrades/new.html.erb
+++ b/app/views/user_upgrades/new.html.erb
@@ -18,6 +18,10 @@
<% else %>
Upgrade Account
+ <% if Danbooru.config.is_promotion? %>
+
Danbooru Winter Sale! Gold and Platinum upgrades are 25% off from now until January 1st.
+ <% end %>
+
Upgrading your account gives you exclusive benefits and helps support
<%= Danbooru.config.canonical_app_name %>. Your support helps keep the
site ad-free for everyone!
@@ -44,10 +48,16 @@
Free
+ <% if Danbooru.config.is_promotion? %>
+ $20
+ <% end %>
<%= cents_to_usd(UserUpgrade.gold_price) %>
One time fee
+ <% if Danbooru.config.is_promotion? %>
+ $40
+ <% end %>
<%= cents_to_usd(UserUpgrade.platinum_price) %>
One time fee
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 75622de73..08f6c29ee 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -494,6 +494,12 @@ module Danbooru
def redis_url
"redis://localhost:6379"
end
+
+ def is_promotion?
+ Time.use_zone("UTC") do
+ Time.zone.now < Time.zone.parse("2021-01-01")
+ end
+ end
end
EnvironmentConfiguration = Struct.new(:config) do
diff --git a/public/images/ablobgift.gif b/public/images/ablobgift.gif
new file mode 100644
index 0000000000000000000000000000000000000000..bca83dc11e986862b0c2ba60c6efeb9b81644b1b
GIT binary patch
literal 210166
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