diff --git a/Gemfile b/Gemfile
index 9f109c6ae..eed0df543 100644
--- a/Gemfile
+++ b/Gemfile
@@ -40,7 +40,8 @@ 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'
gem 'nokogiri'
@@ -59,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'
@@ -85,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 66cc5f2ca..ded3f03a0 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
@@ -5,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)
@@ -77,13 +94,13 @@ 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.414.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)
@@ -120,20 +137,20 @@ GEM
xpath (~> 3.2)
childprocess (3.0.0)
chronic (0.10.2)
- codecov (0.2.12)
- json
- simplecov
+ codecov (0.2.15)
+ simplecov (>= 0.15, < 0.21)
coderay (1.1.3)
concurrent-ruby (1.1.7)
crass (1.0.6)
daemons (1.3.1)
- delayed_job (4.1.8)
- activesupport (>= 3.0, < 6.1)
- delayed_job_active_record (4.1.4)
- activerecord (>= 3.0, < 6.1)
+ dante (0.2.0)
+ 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)
+ docile (1.3.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
@@ -143,11 +160,13 @@ GEM
erubi (1.10.0)
factory_bot (6.1.0)
activesupport (>= 5.0.0)
- faraday (1.1.0)
+ faraday (1.3.0)
+ faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
+ faraday-net_http (1.0.0)
ffaker (2.17.0)
- ffi (1.13.1)
+ ffi (1.14.2)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
@@ -161,19 +180,17 @@ 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
- i18n (1.8.5)
+ i18n (1.8.6)
concurrent-ruby (~> 1.0)
ipaddress_2 (0.13.0)
jmespath (1.4.0)
- json (2.3.1)
+ json (2.5.1)
jwt (2.2.2)
kgio (2.11.3)
- listen (3.3.3)
+ listen (3.4.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.8.0)
@@ -185,9 +202,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)
@@ -200,7 +214,7 @@ GEM
builder
minitest (>= 5.0)
ruby-progressbar
- mocha (1.11.2)
+ mocha (1.12.0)
mock_redis (0.26.0)
msgpack (1.3.3)
multi_json (1.15.0)
@@ -224,7 +238,7 @@ GEM
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
parallel (1.20.1)
- parser (2.7.2.0)
+ parser (3.0.0.0)
ast (~> 2.4.1)
pg (1.2.3)
pry (0.13.1)
@@ -236,48 +250,46 @@ 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)
rack (2.2.3)
- rack-contrib (2.3.0)
- rack (~> 2.0)
- rack-mini-profiler (2.2.0)
+ rack-mini-profiler (2.3.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)
+ rake (13.0.3)
rakismet (1.5.4)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
@@ -292,22 +304,22 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
- rubocop (1.4.2)
+ rubocop (1.7.0)
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)
+ rubocop-ast (1.4.0)
parser (>= 2.7.1.5)
- rubocop-rails (2.8.1)
+ rubocop-rails (2.9.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 0.87.0)
- ruby-progressbar (1.10.1)
+ rubocop (>= 0.90.0, < 2.0)
+ ruby-progressbar (1.11.0)
ruby-vips (2.0.17)
ffi (~> 1.9)
ruby2_keywords (0.0.2)
@@ -349,15 +361,18 @@ 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)
- thread_safe (~> 0.1)
+ tzinfo (2.0.4)
+ concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
- unicorn (5.7.0)
+ unicorn (5.8.0)
kgio (~> 2.6)
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
@@ -382,7 +397,7 @@ PLATFORMS
DEPENDENCIES
activemodel-serializers-xml
- activerecord-hierarchical_query
+ activerecord-hierarchical_query!
addressable
aws-sdk-sqs (~> 1)
bcrypt
@@ -405,12 +420,12 @@ DEPENDENCIES
ffaker
flamegraph
http
+ http-cookie!
ipaddress_2
listen
mail
memoist
memory_profiler
- meta_request
minitest-ci
minitest-reporters
mocha
@@ -446,6 +461,7 @@ DEPENDENCIES
stackprof
streamio-ffmpeg
stripe
+ stripe-ruby-mock
unicorn
unicorn-worker-killer
webpacker (>= 4.0.x)
diff --git a/INSTALL.debian b/INSTALL.debian
index c435d7748..a471b1eeb 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
@@ -76,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
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/application_controller.rb b/app/controllers/application_controller.rb
index 9afec6b03..af1317760 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"
@@ -87,6 +88,8 @@ class ApplicationController < ActionController::Base
end
def rescue_exception(exception)
+ raise exception if Danbooru.config.debug_mode
+
case exception
when ActionView::Template::Error
rescue_exception(exception.cause)
@@ -121,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
@@ -148,6 +151,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/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 37fa3565b..0b36168db 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -2,15 +2,16 @@ 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
+ @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/controllers/emails_controller.rb b/app/controllers/emails_controller.rb
index d9b35a46e..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
@@ -17,7 +28,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/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/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/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 1a63c5b8a..f5fcc40e0 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -9,10 +9,10 @@ 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[: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/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/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/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/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/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb
index 8001b1d88..0a25e49fc 100644
--- a/app/controllers/user_upgrades_controller.rb
+++ b/app/controllers/user_upgrades_controller.rb
@@ -1,64 +1,59 @@
class UserUpgradesController < ApplicationController
- helper_method :user
- skip_before_action :verify_authenticity_token, only: [:create]
+ respond_to :js, :html, :json, :xml
def create
- if params[:stripeToken]
- create_stripe
- end
+ @user_upgrade = authorize UserUpgrade.create(recipient: recipient, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
+ @country = params[:country] || CurrentUser.country || "US"
+ @allow_promotion_codes = params[:promo].to_s.truthy?
+ @checkout = @user_upgrade.create_checkout!(country: @country, allow_promotion_codes: @allow_promotion_codes)
+
+ 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 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
- authorize User, :upgrade?
+ @user_upgrade = authorize UserUpgrade.find(params[:id])
+ respond_with(@user_upgrade)
end
- def user
+ def refund
+ @user_upgrade = authorize UserUpgrade.find(params[:id])
+ @user_upgrade.refund!
+ flash[:notice] = "Upgrade refunded"
+
+ respond_with(@user_upgrade)
+ end
+
+ def receipt
+ @user_upgrade = authorize UserUpgrade.find(params[:id])
+ redirect_to @user_upgrade.receipt_url
+ end
+
+ def payment
+ @user_upgrade = authorize UserUpgrade.find(params[:id])
+ redirect_to @user_upgrade.payment_url
+ end
+
+ private
+
+ def recipient
if params[:user_id]
User.find(params[:user_id])
else
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",
- :card => params[:stripeToken],
- :description => params[:desc]
- )
- @user.promote_to!(level, is_upgrade: true)
- flash[:success] = true
- rescue Stripe::CardError => 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/users_controller.rb b/app/controllers/users_controller.rb
index 71451d04d..f28cd9390 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|
@@ -62,7 +59,7 @@ class UsersController < ApplicationController
end
def create
- requires_verification = IpLookup.new(CurrentUser.ip_addr).is_proxy? || IpBan.hit!(:partial, CurrentUser.ip_addr)
+ requires_verification = UserVerifier.new(CurrentUser.user, request).requires_verification?
@user = authorize User.new(
last_ip_addr: CurrentUser.ip_addr,
@@ -80,7 +77,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/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/application_helper.rb b/app/helpers/application_helper.rb
index b33c763d1..f0c161641 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)
@@ -272,6 +273,7 @@ module ApplicationHelper
{
lang: "en",
class: "c-#{controller_param} a-#{action_param}",
+ spellcheck: "false",
data: {
controller: controller_param,
action: action_param,
diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb
index dc5b4d0c3..a521b20e8 100644
--- a/app/helpers/posts_helper.rb
+++ b/app/helpers/posts_helper.rb
@@ -48,21 +48,10 @@ module PostsHelper
end
end
- def post_favlist(post)
- post.favorited_users.reverse_each.map {|user| link_to_user(user)}.join(", ").html_safe
- end
-
def is_pool_selected?(pool)
return false if params.key?(:q)
return false if params.key?(:favgroup_id)
return false if !params.key?(:pool_id)
return params[:pool_id].to_i == pool.id
end
-
- private
-
- def nav_params_for(page)
- query_params = params.except(:controller, :action, :id).merge(page: page).permit!
- { params: query_params }
- end
end
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/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/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb
index a1d32f478..c3269f358 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
@@ -244,7 +180,7 @@ 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 === "user") {
var level_class = "user-" + item.level.toLowerCase();
@@ -256,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]);
});
@@ -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.json", {
+ "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/javascript/src/javascripts/common.js b/app/javascript/src/javascripts/common.js
index 04e2af233..90fc89347 100644
--- a/app/javascript/src/javascripts/common.js
+++ b/app/javascript/src/javascripts/common.js
@@ -3,7 +3,7 @@ import Cookie from './cookie'
$(function() {
$("#hide-upgrade-account-notice").on("click.danbooru", function(e) {
$("#upgrade-account-notice").hide();
- Cookie.put('hide_upgrade_account_notice', '1', 7);
+ Cookie.put('hide_upgrade_account_notice', '1', 7 * 24 * 60 * 60);
e.preventDefault();
});
@@ -17,7 +17,7 @@ $(function() {
$("#hide-verify-account-notice").on("click.danbooru", function(e) {
$("#verify-account-notice").hide();
- Cookie.put('hide_verify_account_notice', '1', 3);
+ Cookie.put('hide_verify_account_notice', '1', 3 * 24 * 60 * 60);
e.preventDefault();
});
diff --git a/app/javascript/src/javascripts/cookie.js b/app/javascript/src/javascripts/cookie.js
index 20a5e4b0b..11fa8a2d4 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_seconds = 60 * 60 * 24 * 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_seconds) {
+ cookie += ` Max-Age=${max_age_in_seconds};`
}
- var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/";
- 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) {
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 28eef86ec..1e98f1a6f 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);
@@ -91,6 +96,7 @@
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
--autocomplete-border: 1px solid #CCC;
--autocomplete-arrow-color: var(--text-color);
+ --autocomplete-tag-autocorrect-underline: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAHElEQVQYV2NkQAL/GRj+M4IJBgY4zQhSABMEsQHMOAgCT5YN9gAAAABJRU5ErkJggg==);
--diff-list-added-color: green;
--diff-list-removed-color: red;
@@ -157,7 +163,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);
@@ -201,6 +208,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;
@@ -252,6 +262,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);
@@ -291,6 +302,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);
@@ -413,9 +427,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/common/autocomplete.scss b/app/javascript/src/styles/common/autocomplete.scss
index 6ca29663e..b90dbd9e2 100644
--- a/app/javascript/src/styles/common/autocomplete.scss
+++ b/app/javascript/src/styles/common/autocomplete.scss
@@ -21,4 +21,15 @@
.autocomplete-arrow {
color: var(--autocomplete-arrow-color);
}
+
+ /* Display a red wavy underline beneath misspelled tags. */
+ /* https://stackoverflow.com/a/28152272 */
+ li[data-autocomplete-type="tag-autocorrect"] .autocomplete-antecedent {
+ position: relative;
+ display: inline-block;
+ background: var(--autocomplete-tag-autocorrect-underline);
+ background-repeat: repeat-x;
+ background-position-y: 1.2em;
+ line-height: 1.5em;
+ }
}
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;
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/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/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/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/logical/artist_finder.rb b/app/logical/artist_finder.rb
index a0e8f21b9..b2ca33a9e 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",
@@ -60,6 +61,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
@@ -83,8 +85,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",
diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb
new file mode 100644
index 000000000..b870755b5
--- /dev/null
+++ b/app/logical/autocomplete_service.rb
@@ -0,0 +1,258 @@
+class AutocompleteService
+ extend Memoist
+
+ 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_s.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)
+ if string.starts_with?("/")
+ string = string + "*" unless string.include?("*")
+
+ results = tag_matches(string)
+ results += tag_abbreviation_matches(string)
+ 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)
+ results = tag_other_name_matches(string) if results.blank?
+ else
+ string += "*"
+ results = tag_matches(string)
+ results = tag_other_name_matches(string) if results.blank?
+ results = tag_autocorrect_matches(string) if results.blank?
+ end
+
+ results
+ 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"
+ tags = Tag.from(union).order(post_count: :desc).limit(limit).includes(:consequent_aliases)
+
+ tags.map do |tag|
+ 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
+
+ def tag_abbreviation_matches(string)
+ tags = Tag.nonempty.abbreviation_matches(string).order(post_count: :desc).limit(limit)
+
+ tags.map do |tag|
+ { type: "tag-abbreviation", 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)
+ string = string.delete("*")
+ 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 }
+ 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-other-name", 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,
+ :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)
+ else
+ []
+ 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)
+ string = "*" + string + "*" unless string.include?("*")
+ labels = current_user.saved_searches.labels_like(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
+
+ 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
diff --git a/app/logical/bulk_update_request_processor.rb b/app/logical/bulk_update_request_processor.rb
index 25b96918e..ea77175fb 100644
--- a/app/logical/bulk_update_request_processor.rb
+++ b/app/logical/bulk_update_request_processor.rb
@@ -1,5 +1,11 @@
class BulkUpdateRequestProcessor
+ # Maximum tag size allowed by the rename command before an alias must be used.
MAXIMUM_RENAME_COUNT = 200
+
+ # Maximum size of artist tags movable by builders.
+ MAXIMUM_BUILDER_MOVE_COUNT = 200
+
+ # Maximum number of lines a BUR may have.
MAXIMUM_SCRIPT_LENGTH = 100
include ActiveModel::Validations
@@ -55,20 +61,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 +82,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 +90,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 +119,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
@@ -212,11 +218,18 @@ class BulkUpdateRequestProcessor
end.join("\n")
end
+ # Tag move is allowed if:
+ #
+ # * The antecedent tag is an artist tag.
+ # * The consequent_tag is a nonexistent tag, an empty tag (of any type), or an artist tag.
+ # * Both tags have less than 200 posts.
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
- (antecedent_tag.blank? || antecedent_tag.empty? || (antecedent_tag.artist? && antecedent_tag.post_count <= 100)) &&
- (consequent_tag.blank? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count <= 100))
+ antecedent_allowed = antecedent_tag.present? && antecedent_tag.artist? && antecedent_tag.post_count < MAXIMUM_BUILDER_MOVE_COUNT
+ consequent_allowed = consequent_tag.nil? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count < MAXIMUM_BUILDER_MOVE_COUNT)
+
+ antecedent_allowed && consequent_allowed
end
end
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/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb
index 151ad5f86..b1f9610b6 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?
@@ -52,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
@@ -101,16 +102,11 @@ module Searchable
where_operator(qualified_column, *range)
end
- def search_boolean_attribute(attribute, params)
- return all unless params.key?(attribute)
-
- value = params[attribute].to_s
- if value.truthy?
- where(attribute => true)
- elsif value.falsy?
- where(attribute => false)
+ def search_boolean_attribute(attr, params)
+ if params[attr].present?
+ boolean_attribute_matches(attr, params[attr])
else
- raise ArgumentError, "value must be truthy or falsy"
+ all
end
end
@@ -132,6 +128,18 @@ module Searchable
where_operator(qualified_column, *range)
end
+ def boolean_attribute_matches(attribute, value)
+ value = value.to_s
+
+ if value.truthy?
+ where(attribute => true)
+ elsif value.falsy?
+ where(attribute => false)
+ else
+ raise ArgumentError, "value must be truthy or falsy"
+ end
+ end
+
def text_attribute_matches(attribute, value, index_column: nil, ts_config: "english")
return all unless value.present?
@@ -182,7 +190,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
@@ -195,6 +203,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])
@@ -385,14 +413,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/logical/current_user.rb b/app/logical/current_user.rb
index 4c8d80349..5145ec7a7 100644
--- a/app/logical/current_user.rb
+++ b/app/logical/current_user.rb
@@ -40,6 +40,14 @@ class CurrentUser
RequestStore[:current_ip_addr] = ip_addr
end
+ def self.country
+ RequestStore[:country]
+ end
+
+ def self.country=(country)
+ RequestStore[:country] = country
+ end
+
def self.root_url
RequestStore[:current_root_url] || "https://#{Danbooru.config.hostname}"
end
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/danbooru/http.rb b/app/logical/danbooru/http.rb
index e1b76b16b..bbf30cafd 100644
--- a/app/logical/danbooru/http.rb
+++ b/app/logical/danbooru/http.rb
@@ -1,3 +1,4 @@
+require "danbooru/http/application_client"
require "danbooru/http/html_adapter"
require "danbooru/http/xml_adapter"
require "danbooru/http/cache"
diff --git a/app/logical/danbooru_logger.rb b/app/logical/danbooru_logger.rb
index 01422f727..30989d162 100644
--- a/app/logical/danbooru_logger.rb
+++ b/app/logical/danbooru_logger.rb
@@ -1,4 +1,6 @@
class DanbooruLogger
+ HEADERS = %w[referer sec-fetch-dest sec-fetch-mode sec-fetch-site sec-fetch-user]
+
def self.info(message, params = {})
Rails.logger.info(message)
@@ -22,25 +24,52 @@ class DanbooruLogger
end
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? }
+ add_attributes("request", { path: request.path })
+ add_attributes("request.headers", header_params(request))
+ add_attributes("request.params", request_params(request))
+ add_attributes("session.params", session_params(session))
+ add_attributes("user", user_params(request, user))
+ end
- add_attributes("request.params", request_params)
- add_attributes("session.params", session_params)
- add_attributes("user", user_params)
+ def self.header_params(request)
+ headers = request.headers.to_h.select { |header, value| header.match?(/\AHTTP_/) }
+ headers = headers.transform_keys { |header| header.delete_prefix("HTTP_").downcase }
+ headers = headers.select { |header, value| header.in?(HEADERS) }
+ headers
+ end
+
+ def self.request_params(request)
+ request.parameters.with_indifferent_access.except(:controller, :action)
+ end
+
+ def self.session_params(session)
+ session.to_h.with_indifferent_access.slice(:session_id, :started_at)
+ end
+
+ def self.user_params(request, user)
+ {
+ id: user&.id,
+ name: user&.name,
+ level: user&.level_string,
+ ip: request.remote_ip,
+ country: CurrentUser.country,
+ safe_mode: CurrentUser.safe_mode?
+ }
end
def self.add_attributes(prefix, hash)
- return unless defined?(::NewRelic)
-
attributes = flatten_hash(hash).transform_keys { |key| "#{prefix}.#{key}" }
attributes.delete_if { |key, value| key.end_with?(*Rails.application.config.filter_parameters.map(&:to_s)) }
- ::NewRelic::Agent.add_custom_attributes(attributes)
+ add_custom_attributes(attributes)
end
private_class_method
+ def self.add_custom_attributes(attributes)
+ return unless defined?(::NewRelic)
+ ::NewRelic::Agent.add_custom_attributes(attributes)
+ end
+
# flatten_hash({ foo: { bar: { baz: 42 } } })
# => { "foo.bar.baz" => 42 }
def self.flatten_hash(hash)
diff --git a/app/logical/dtext_input.rb b/app/logical/dtext_input.rb
index a1910e933..77e55621a 100644
--- a/app/logical/dtext_input.rb
+++ b/app/logical/dtext_input.rb
@@ -16,7 +16,7 @@ class DtextInput < SimpleForm::Inputs::Base
t = template
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
- t.tag.div(class: "dtext-previewable") do
+ t.tag.div(class: "dtext-previewable", spellcheck: true) do
if options[:inline]
t.concat @builder.text_field(attribute_name, merged_input_options)
else
diff --git a/app/logical/email_validator.rb b/app/logical/email_validator.rb
index 35fd5c46a..6e4b60194 100644
--- a/app/logical/email_validator.rb
+++ b/app/logical/email_validator.rb
@@ -3,6 +3,9 @@ require 'resolv'
module EmailValidator
module_function
+ # https://www.regular-expressions.info/email.html
+ EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
+
IGNORE_DOTS = %w[gmail.com]
IGNORE_PLUS_ADDRESSING = %w[gmail.com hotmail.com outlook.com live.com]
IGNORE_MINUS_ADDRESSING = %w[yahoo.com]
@@ -80,10 +83,17 @@ module EmailValidator
"#{name}@#{domain}"
end
- def nondisposable?(address)
- return true if Danbooru.config.email_domain_verification_list.blank?
+ def is_valid?(address)
+ address.match?(EMAIL_REGEX)
+ end
+
+ def is_restricted?(address)
+ return false if Danbooru.config.email_domain_verification_list.blank?
+
domain = Mail::Address.new(address).domain
- domain.in?(Danbooru.config.email_domain_verification_list.to_a)
+ !domain.in?(Danbooru.config.email_domain_verification_list.to_a)
+ rescue Mail::Field::IncompleteParseError
+ true
end
def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3)
diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb
index 19c67138e..8e6fc980f 100644
--- a/app/logical/post_query_builder.rb
+++ b/app/logical/post_query_builder.rb
@@ -3,6 +3,9 @@ require "strscan"
class PostQueryBuilder
extend Memoist
+ # How many tags a `blah*` search should match.
+ MAX_WILDCARD_TAGS = 100
+
COUNT_METATAGS = %w[
comment_count deleted_comment_count active_comment_count
note_count deleted_note_count active_note_count
@@ -77,9 +80,9 @@ class PostQueryBuilder
optional_tags = optional_tags.map(&:name)
required_tags = required_tags.map(&:name)
- negated_tags += negated_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
- optional_tags += optional_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
- optional_tags += required_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name) }
+ negated_tags += negated_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
+ optional_tags += optional_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
+ optional_tags += required_wildcard_tags.flat_map { |tag| Tag.wildcard_matches(tag.name).limit(MAX_WILDCARD_TAGS).pluck(:name) }
tsquery << "!(#{negated_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if negated_tags.present?
tsquery << "(#{optional_tags.sort.uniq.map(&:to_escaped_for_tsquery).join(" | ")})" if optional_tags.present?
@@ -92,8 +95,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
@@ -390,7 +393,8 @@ class PostQueryBuilder
favuser = User.find_by_name(username)
if favuser.present? && Pundit.policy!([current_user, nil], favuser).can_see_favorites?
- tags_include("fav:#{favuser.id}")
+ favorites = Favorite.from("favorites_#{favuser.id % 100} AS favorites").where(user: favuser)
+ Post.where(id: favorites.select(:post_id))
else
Post.none
end
@@ -399,8 +403,8 @@ class PostQueryBuilder
def ordfav_matches(username)
user = User.find_by_name(username)
- if user.present?
- favorites_include(username).joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC")
+ if user.present? && Pundit.policy!([current_user, nil], user).can_see_favorites?
+ Post.joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC")
else
Post.none
end
@@ -985,6 +989,11 @@ class PostQueryBuilder
def is_wildcard_search?
is_single_tag? && tags.first.wildcard
end
+
+ def simple_tag
+ return nil if !is_simple_tag?
+ Tag.find_by_name(tags.first.name)
+ end
end
memoize :split_query, :normalized_query
diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb
index ff48e9c4c..69f1fcf5c 100644
--- a/app/logical/post_sets/post.rb
+++ b/app/logical/post_sets/post.rb
@@ -187,16 +187,19 @@ module PostSets
RelatedTagCalculator.frequent_tags_for_post_array(posts).take(MAX_SIDEBAR_TAGS)
end
+ # Wildcard searches can show up to 100 tags in the sidebar, not 25,
+ # because that's how many tags the search itself will use.
def wildcard_tags
- Tag.wildcard_matches(tag_string)
+ Tag.wildcard_matches(tag_string).limit(PostQueryBuilder::MAX_WILDCARD_TAGS).pluck(:name)
end
def saved_search_tags
- ["search:all"] + SavedSearch.labels_for(CurrentUser.user.id).map {|x| "search:#{x}"}
+ searches = ["search:all"] + SavedSearch.labels_for(CurrentUser.user.id).map {|x| "search:#{x}"}
+ searches.take(MAX_SIDEBAR_TAGS)
end
def tag_set_presenter
- @tag_set_presenter ||= TagSetPresenter.new(related_tags.take(MAX_SIDEBAR_TAGS))
+ @tag_set_presenter ||= TagSetPresenter.new(related_tags)
end
def tag_list_html(**options)
diff --git a/app/logical/related_tag_query.rb b/app/logical/related_tag_query.rb
index 6a86b10b0..fb6c71413 100644
--- a/app/logical/related_tag_query.rb
+++ b/app/logical/related_tag_query.rb
@@ -71,9 +71,12 @@ class RelatedTagQuery
end
def other_wiki_pages
- if Tag.category_for(query) == Tag.categories.copyright
+ tag = post_query.simple_tag
+ return [] if tag.nil?
+
+ if tag.copyright?
copyright_other_wiki_pages
- elsif Tag.category_for(query) == Tag.categories.general
+ elsif tag.general?
general_other_wiki_pages
else
[]
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/server_status.rb b/app/logical/server_status.rb
new file mode 100644
index 000000000..e3a903230
--- /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
+ `. /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/logical/session_loader.rb b/app/logical/session_loader.rb
index d4ecf45d1..12f12e189 100644
--- a/app/logical/session_loader.rb
+++ b/app/logical/session_loader.rb
@@ -34,6 +34,7 @@ class SessionLoader
update_last_logged_in_at
update_last_ip_addr
set_time_zone
+ set_country
set_safe_mode
initialize_session_cookies
CurrentUser.user.unban! if CurrentUser.user.ban_expired?
@@ -87,7 +88,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
@@ -101,6 +102,12 @@ class SessionLoader
Time.zone = CurrentUser.user.time_zone
end
+ # Depends on Cloudflare
+ # https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation
+ def set_country
+ CurrentUser.country = request.headers["CF-IPCountry"]
+ end
+
def set_safe_mode
safe_mode = request.host.match?(/safebooru/i) || params[:safe_mode].to_s.truthy? || CurrentUser.user.enable_safe_mode?
CurrentUser.safe_mode = safe_mode
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/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"]
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/logical/tag_name_validator.rb b/app/logical/tag_name_validator.rb
index b4886f33d..029a8c5c0 100644
--- a/app/logical/tag_name_validator.rb
+++ b/app/logical/tag_name_validator.rb
@@ -1,38 +1,40 @@
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[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 (',')"
- when /\A~/
- record.errors[attribute] << "'#{value}' cannot begin with a tilde ('~')"
- when /\A-/
- record.errors[attribute] << "'#{value}' cannot begin with a dash ('-')"
- when /\A_/
- record.errors[attribute] << "'#{value}' cannot begin with an underscore"
+ record.errors.add(attribute, "'#{value}' cannot contain commas (',')")
+ when /\A[-~_`%){}\]\/]/
+ record.errors.add(attribute, "'#{value}' cannot begin with a '#{value[0]}'")
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/upload_service/replacer.rb b/app/logical/upload_service/replacer.rb
index 2015e235b..3d19a86ad 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 file:\n\n#{replacement_message(post, replacement)})
+ %("#{replacement.creator.name}":[#{Routes.user_path(replacement.creator)}] replaced this post with a new file:\n\n#{replacement_message(post, replacement)})
end
def replacement_message(post, replacement)
diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb
index 8577f19a1..e39192736 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"
+ if user.is_admin?
+ 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/logical/user_promotion.rb b/app/logical/user_promotion.rb
index b3b7a32bb..2ee8ee685 100644
--- a/app/logical/user_promotion.rb
+++ b/app/logical/user_promotion.rb
@@ -1,33 +1,27 @@
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
- def initialize(user, promoter, new_level, options = {})
+ def initialize(user, promoter, new_level, can_upload_free: nil, can_approve_posts: nil)
@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
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
+ create_dmail
create_mod_actions
user.save
@@ -37,28 +31,28 @@ 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 = options[: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}}, :user_level_change, promoter)
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/logical/user_upgrade.rb b/app/logical/user_upgrade.rb
deleted file mode 100644
index b29909faf..000000000
--- a/app/logical/user_upgrade.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class UserUpgrade
- def self.gold_price
- 2000
- end
-
- def self.platinum_price
- 4000
- end
-
- def self.upgrade_price
- 2000
- end
-end
diff --git a/app/logical/user_verifier.rb b/app/logical/user_verifier.rb
new file mode 100644
index 000000000..d1e5991b9
--- /dev/null
+++ b/app/logical/user_verifier.rb
@@ -0,0 +1,51 @@
+# Checks whether a new account seems suspicious and should require email verification.
+
+class UserVerifier
+ attr_reader :current_user, :request
+
+ # current_user is the user creating the new account, not the new account itself.
+ def initialize(current_user, request)
+ @current_user, @request = current_user, request
+ end
+
+ def requires_verification?
+ return false if !Danbooru.config.new_user_verification?
+ return false if is_local_ip?
+
+ # we check for IP bans first to make sure we bump the IP ban hit count
+ is_ip_banned? || is_logged_in? || is_recent_signup? || is_proxy?
+ end
+
+ private
+
+ def ip_address
+ @ip_address ||= IPAddress.parse(request.remote_ip)
+ end
+
+ def is_local_ip?
+ if ip_address.ipv4?
+ ip_address.loopback? || ip_address.link_local? || ip_address.private?
+ elsif ip_address.ipv6?
+ ip_address.loopback? || ip_address.link_local? || ip_address.unique_local?
+ end
+ end
+
+ def is_logged_in?
+ !current_user.is_anonymous?
+ end
+
+ def is_recent_signup?(age: 24.hours)
+ subnet_len = ip_address.ipv4? ? 24 : 64
+ subnet = "#{ip_address}/#{subnet_len}"
+
+ User.where("last_ip_addr <<= ?", subnet).where("created_at > ?", age.ago).exists?
+ end
+
+ def is_ip_banned?
+ IpBan.hit!(:partial, ip_address.to_s)
+ end
+
+ def is_proxy?
+ IpLookup.new(ip_address).is_proxy?
+ end
+end
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
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index c65ae40d1..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
@@ -93,10 +94,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 cc604789c..56f1baee8 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
@@ -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
@@ -246,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, :urls, :wiki_page, :tag_alias, :tag)
if params[:any_other_name_like]
q = q.any_other_name_like(params[:any_other_name_like])
@@ -293,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 d94939d22..d1b5bcee6 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, :post)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
@@ -146,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 1bf34aa02..0b4c00436 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, :post, :updater)
if params[:text_matches].present?
q = q.text_matches(params[:text_matches])
@@ -56,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 625357dac..fa9b359fb 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, :artist)
q = q.url_matches(params[:url_matches])
q = q.normalized_url_matches(params[:normalized_url_matches])
@@ -113,11 +111,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,11 +123,7 @@ class ArtistUrl < ApplicationRecord
validate_scheme(uri)
validate_hostname(uri)
rescue Addressable::URI::InvalidURIError => error
- errors[:url] << "'#{uri}' is malformed: #{error}"
- end
-
- def self.searchable_includes
- [:artist]
+ errors.add(:url, "'#{uri}' is malformed: #{error}")
end
def self.available_includes
diff --git a/app/models/artist_version.rb b/app/models/artist_version.rb
index 19d608e6b..a743c43ed 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, :updater, :artist)
q = q.text_attribute_matches(:name, params[:name_matches])
q = q.text_attribute_matches(:group_name, params[:group_name_matches])
@@ -105,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 408a33306..c641b4d5a 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, :user, :banner)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q = q.expired if params[:expired].to_s.truthy?
@@ -45,7 +43,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
@@ -89,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 2149b564f..b40bcabca 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, :user, :forum_topic, :forum_post, :approver)
q = q.text_attribute_matches(:script, params[:script_matches])
if params[:status].present?
@@ -91,13 +89,13 @@ 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
def validate_script
if processor.invalid?(:request)
- errors[:base] << processor.errors.full_messages.join("; ")
+ errors.add(:base, processor.errors.full_messages.join("; "))
end
end
@@ -128,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 0c4fd1f8d..c56271f56 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -21,14 +21,12 @@ 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
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, :post, :creator, :updater)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
case params[:order]
@@ -139,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 ec1a5ce11..b87642d10 100644
--- a/app/models/comment_vote.rb
+++ b/app/models/comment_vote.rb
@@ -19,14 +19,13 @@ 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, :comment, :user)
q.apply_default_order(params)
end
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
@@ -38,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 a789384d9..63094bff5 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, :to, :from)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index)
@@ -158,7 +156,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
@@ -182,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 ea4902d53..3d3f8d62b 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, :model, :linked_wiki, :linked_tag)
q.apply_default_order(params)
end
@@ -49,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/email_address.rb b/app/models/email_address.rb
index 6267ff2bc..f0420a734 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -1,32 +1,67 @@
class EmailAddress < ApplicationRecord
- # https://www.regular-expressions.info/email.html
- EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
-
belongs_to :user, inverse_of: :email_address
- validates :address, presence: true, confirmation: true, format: { with: EMAIL_REGEX }
+ validates :address, presence: true, confirmation: true, format: { with: EmailValidator::EMAIL_REGEX }
validates :normalized_address, uniqueness: true
validates :user_id, uniqueness: true
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(User.where(id: user.id)))
+ else
+ none
+ end
+ end
+
def address=(value)
self.normalized_address = EmailValidator.normalize(value) || address
super
end
- def nondisposable?
- EmailValidator.nondisposable?(normalized_address)
+ def is_restricted?
+ EmailValidator.is_restricted?(normalized_address)
+ end
+
+ def is_normalized?
+ address == normalized_address
+ end
+
+ def is_valid?
+ EmailValidator.is_valid?(address)
+ end
+
+ def self.restricted(restricted = true)
+ domains = Danbooru.config.email_domain_verification_list
+ domain_regex = domains.map { |domain| Regexp.escape(domain) }.join("|")
+
+ if restricted.to_s.truthy?
+ where_not_regex(:normalized_address, "@(#{domain_regex})$")
+ elsif restricted.to_s.falsy?
+ where_regex(:normalized_address, "@(#{domain_regex})$")
+ else
+ all
+ end
+ end
+
+ def self.search(params)
+ q = search_attributes(params, :id, :created_at, :updated_at, :user, :address, :normalized_address, :is_verified, :is_deliverable)
+
+ q = q.restricted(params[:is_restricted])
+ q = q.apply_default_order(params)
+
+ q
end
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
def update_user
- user.update!(is_verified: is_verified? && nondisposable?)
+ user.update!(is_verified: is_verified? && !is_restricted?)
end
concerning :VerificationMethods do
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 edd4d0dfe..5a0c07190 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, :creator)
if params[:name_matches].present?
q = q.name_matches(params[:name_matches])
@@ -56,13 +55,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 +71,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
@@ -164,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 62815ff62..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
@@ -36,13 +36,16 @@ 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 = 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?
- 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)
@@ -160,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 3ea592389..6a2ac7926 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, :creator, :forum_post)
q = q.forum_post_matches(params[:forum_post])
q.apply_default_order(params)
end
@@ -59,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 f44a727d6..0ab60d042 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, :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?
@@ -190,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/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..77f44b3ff 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, :user, :model)
q.order(created_at: :desc)
end
@@ -50,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 266ff0ea0..d69f41bbf 100644
--- a/app/models/ip_ban.rb
+++ b/app/models/ip_ban.rb
@@ -25,15 +25,18 @@ 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, :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
@@ -48,19 +51,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
@@ -78,10 +81,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 87fbfda97..437d3efea 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, :creator)
q = q.text_attribute_matches(:description, params[:description_matches])
q.apply_default_order(params)
@@ -77,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 4fa1f77de..20874e2d1 100644
--- a/app/models/moderation_report.rb
+++ b/app/models/moderation_report.rb
@@ -82,17 +82,12 @@ 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, :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 4facb2028..f2cc46ed3 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, :post)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
q.apply_default_order(params)
@@ -26,13 +24,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
@@ -129,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 d8c3aed07..2b973650f 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, :updater, :note, :post)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
@@ -71,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 92c1eba2b..bbb5a5498 100644
--- a/app/models/pixiv_ugoira_frame_data.rb
+++ b/app/models/pixiv_ugoira_frame_data.rb
@@ -4,17 +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 = super
- q = q.search_attributes(params, :data, :content_type)
+ q = search_attributes(params, :id, :data, :content_type, :post)
q.apply_default_order(params)
end
diff --git a/app/models/pool.rb b/app/models/pool.rb
index e789f10a7..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]
@@ -147,7 +145,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 +252,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/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 8a33ac746..a4bfc70e8 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
@@ -55,6 +56,7 @@ class Post < ApplicationRecord
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
has_many :favorites
+ has_many :favorited_users, through: :favorites, source: :user
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning, :view_count
@@ -490,7 +492,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
@@ -760,12 +762,11 @@ class Post < ApplicationRecord
update_column(:fav_count, fav_count)
end
- def favorited_by?(user_id = CurrentUser.id)
- fav_string.match?(/(?:\A| )fav:#{user_id}(?:\Z| )/)
+ def favorited_by?(user)
+ return false if user.is_anonymous?
+ Favorite.exists?(post: self, user: user)
end
- alias is_favorited? favorited_by?
-
def append_user_to_fav_string(user_id)
update_column(:fav_string, (fav_string + " fav:#{user_id}").strip)
clean_fav_string! if clean_fav_string?
@@ -801,14 +802,11 @@ class Post < ApplicationRecord
false
end
- # users who favorited this post, ordered by users who favorited it first
- def favorited_users
- favorited_user_ids = fav_string.scan(/\d+/).map(&:to_i)
- visible_users = User.find(favorited_user_ids).select do |user|
- Pundit.policy!([CurrentUser.user, nil], user).can_see_favorites?
+ # Users who publicly favorited this post, ordered by time of favorite.
+ def visible_favorited_users(viewer)
+ favorited_users.order("favorites.id DESC").select do |fav_user|
+ Pundit.policy!([viewer, nil], fav_user).can_see_favorites?
end
- ordered_users = visible_users.index_by(&:id).slice(*favorited_user_ids).values
- ordered_users
end
def favorite_groups
@@ -976,7 +974,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 +1080,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
@@ -1289,14 +1287,17 @@ 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,
+ :uploader, :updater, :approver, :parent, :upload, :artist_commentary,
+ :flags, :appeals, :notes, :comments, :children, :approvals,
+ :replacements, :pixiv_ugoira_frame_data
)
if params[:tags].present?
@@ -1357,8 +1358,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,30 +1372,23 @@ 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
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
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
- 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(", ")}"
+ 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)}]"
+ 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
@@ -1406,7 +1399,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
@@ -1417,21 +1410,22 @@ 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"
+ 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
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
@@ -1508,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 8c8be655a..e7ce3d069 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, :creator, :post)
q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params)
@@ -28,15 +27,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?
- end
-
- def self.searchable_includes
- [:creator, :post]
+ errors.add(:post, "cannot be appealed") if post.is_status_locked? || !post.is_appealable?
end
def self.available_includes
diff --git a/app/models/post_approval.rb b/app/models/post_approval.rb
index 599cd1ba9..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 = super
+ 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 0c6af91d8..eccf6f41c 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, :user, :post)
q = q.text_attribute_matches(:message, params[:message_matches])
q = q.with_message if params[:has_message].to_s.truthy?
@@ -41,17 +39,13 @@ class PostDisapproval < ApplicationRecord
end
end
- def self.searchable_includes
- [:user, :post]
- end
-
def self.available_includes
[:user, :post]
end
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..647d852ed 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, :post)
q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:creator_id].present?
@@ -95,17 +93,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
@@ -113,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 35dd266a6..290e54984 100644
--- a/app/models/post_replacement.rb
+++ b/app/models/post_replacement.rb
@@ -11,18 +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 = 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, :old_md5, :file_ext, :old_file_ext, :original_url, :replacement_url, :creator, :post)
q.apply_default_order(params)
end
end
@@ -39,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_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..ab8ea1090 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, :user, :post)
q.apply_default_order(params)
end
@@ -50,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/saved_search.rb b/app/models/saved_search.rb
index e384da9a1..fbe92a248 100644
--- a/app/models/saved_search.rb
+++ b/app/models/saved_search.rb
@@ -63,17 +63,12 @@ class SavedSearch < ApplicationRecord
.gsub(/[[:space:]]/, "_")
end
- def search_labels(user_id, params)
- labels = labels_for(user_id)
+ def all_labels
+ select(Arel.sql("distinct unnest(labels) as label")).order(:label)
+ end
- 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
+ def labels_like(label)
+ all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
end
def labels_for(user_id)
@@ -105,8 +100,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])
@@ -174,7 +168,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.rb b/app/models/tag.rb
index c9ca52da9..6106b774e 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"
@@ -125,18 +127,6 @@ class Tag < ApplicationRecord
Tag.where(name: tag_name).pick(:category).to_i
end
- def category_for(tag_name, options = {})
- return Tag.categories.general if tag_name.blank?
-
- if options[:disable_caching]
- select_category_for(tag_name)
- else
- Cache.get("tc:#{Cache.hash(tag_name)}") do
- select_category_for(tag_name)
- end
- end
- end
-
def categories_for(tag_names, options = {})
if options[:disable_caching]
Array(tag_names).inject({}) do |hash, tag_name|
@@ -232,16 +222,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)
@@ -249,21 +242,29 @@ 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)
name_matches(name).or(alias_matches(name))
end
- def wildcard_matches(tag, limit: 25)
- nonempty.name_matches(tag).order(post_count: :desc, name: :asc).limit(limit).pluck(:name)
+ def wildcard_matches(tag)
+ nonempty.name_matches(tag).order(post_count: :desc, name: :asc)
+ 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 find_by_abbreviation(abbrev)
+ abbrev = abbrev.delete_prefix("/")
+ abbreviation_matches(abbrev.escape_wildcards).order(post_count: :desc).first
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, :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])
@@ -352,12 +353,20 @@ class Tag < ApplicationRecord
Post.system_tag_match(name)
end
- def self.model_restriction(table)
- super.where(table[:post_count].gt(0))
+ def abbreviation
+ name.gsub(ABBREVIATION_REGEXP, "\\1")
end
- def self.searchable_includes
- [:wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links]
+ 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
def self.available_includes
diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb
index 6657b3868..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
@@ -23,7 +31,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..28e6bacb1 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, :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])
@@ -114,7 +113,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
@@ -126,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 e3e8cda1a..6c2878076 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
@@ -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, :uploader, :post)
if params[:source_matches].present?
q = q.where_like(:source, params[:source_matches])
@@ -225,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 5ec821bcb..9b47b01d6 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.
@@ -98,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
@@ -191,6 +194,10 @@ class User < ApplicationRecord
extend ActiveSupport::Concern
module ClassMethods
+ def owner
+ User.find_by!(level: Levels::OWNER)
+ end
+
def system
User.find_by!(name: Danbooru.config.system_user)
end
@@ -208,7 +215,8 @@ class User < ApplicationRecord
"Platinum" => Levels::PLATINUM,
"Builder" => Levels::BUILDER,
"Moderator" => Levels::MODERATOR,
- "Admin" => Levels::ADMIN
+ "Admin" => Levels::ADMIN,
+ "Owner" => Levels::OWNER
}
end
@@ -235,14 +243,17 @@ class User < ApplicationRecord
when Levels::ADMIN
"Admin"
+ when Levels::OWNER
+ "Owner"
+
else
""
end
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
@@ -299,6 +310,10 @@ class User < ApplicationRecord
level >= Levels::ADMIN
end
+ def is_owner?
+ level >= Levels::OWNER
+ end
+
def is_approver?
can_approve_posts?
end
@@ -345,15 +360,90 @@ class User < ApplicationRecord
end
end
- module LimitMethods
- extend Memoist
+ concerning :LimitMethods do
+ class_methods do
+ def statement_timeout(level)
+ if Rails.env.development?
+ 60_000
+ elsif level >= User::Levels::PLATINUM
+ 9_000
+ elsif level == User::Levels::GOLD
+ 6_000
+ else
+ 3_000
+ end
+ end
+
+ def tag_query_limit(level)
+ if level >= User::Levels::BUILDER
+ Float::INFINITY
+ elsif level == User::Levels::PLATINUM
+ 12
+ elsif level == User::Levels::GOLD
+ 6
+ else
+ 2
+ end
+ end
+
+ def favorite_limit(level)
+ if level >= User::Levels::PLATINUM
+ Float::INFINITY
+ elsif level == User::Levels::GOLD
+ 20_000
+ else
+ 10_000
+ end
+ end
+
+ def favorite_group_limit(level)
+ if level >= User::Levels::BUILDER
+ Float::INFINITY
+ elsif level == User::Levels::PLATINUM
+ 10
+ elsif level == User::Levels::GOLD
+ 5
+ else
+ 3
+ end
+ end
+
+ def max_saved_searches(level)
+ if level >= User::Levels::BUILDER
+ Float::INFINITY
+ elsif level == User::Levels::PLATINUM
+ 1_000
+ else
+ 250
+ end
+ end
+
+ # regen this amount per second
+ def api_regen_multiplier(level)
+ if level >= User::Levels::PLATINUM
+ 4
+ elsif level == User::Levels::GOLD
+ 2
+ else
+ 1
+ end
+ end
+
+ # can make this many api calls at once before being bound by
+ # api_regen_multiplier refilling your pool
+ def api_burst_limit(level)
+ if level >= User::Levels::PLATINUM
+ 60
+ elsif level == User::Levels::GOLD
+ 30
+ else
+ 10
+ end
+ end
+ end
def max_saved_searches
- if is_platinum?
- 1_000
- else
- 250
- end
+ User.max_saved_searches(level)
end
def is_comment_limited?
@@ -389,56 +479,23 @@ class User < ApplicationRecord
end
def tag_query_limit
- if is_platinum?
- Danbooru.config.base_tag_query_limit * 2
- elsif is_gold?
- Danbooru.config.base_tag_query_limit
- else
- 2
- end
+ User.tag_query_limit(level)
end
def favorite_limit
- if is_platinum?
- Float::INFINITY
- elsif is_gold?
- 20_000
- else
- 10_000
- end
+ User.favorite_limit(level)
end
def favorite_group_limit
- if is_platinum?
- 10
- elsif is_gold?
- 5
- else
- 3
- end
+ User.favorite_group_limit(level)
end
def api_regen_multiplier
- # regen this amount per second
- if is_platinum?
- 4
- elsif is_gold?
- 2
- else
- 1
- end
+ User.api_regen_multiplier(level)
end
def api_burst_limit
- # can make this many api calls at once before being bound by
- # api_regen_multiplier refilling your pool
- if is_platinum?
- 60
- elsif is_gold?
- 30
- else
- 10
- end
+ User.api_burst_limit(level)
end
def remaining_api_limit
@@ -446,15 +503,7 @@ class User < ApplicationRecord
end
def statement_timeout
- if Rails.env.development?
- 60_000
- elsif is_platinum?
- 9_000
- elsif is_gold?
- 6_000
- else
- 3_000
- end
+ User.statement_timeout(level)
end
end
@@ -470,15 +519,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
@@ -547,12 +587,17 @@ 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, :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]))
@@ -599,7 +644,6 @@ class User < ApplicationRecord
include LevelMethods
include EmailMethods
include ForumMethods
- include LimitMethods
include ApiMethods
include CountMethods
extend SearchMethods
@@ -620,10 +664,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 eaf24fcf4..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
@@ -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, :creator, :user)
q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params)
@@ -54,14 +52,10 @@ 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
- def self.searchable_includes
- [:creator, :user]
- end
-
def self.available_includes
[:creator, :user]
end
diff --git a/app/models/user_name_change_request.rb b/app/models/user_name_change_request.rb
index 2cdafe5a6..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
@@ -30,7 +29,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/user_upgrade.rb b/app/models/user_upgrade.rb
new file mode 100644
index 000000000..de81fa214
--- /dev/null
+++ b/app/models/user_upgrade.rb
@@ -0,0 +1,310 @@
+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,
+ refunded: 30,
+ }
+
+ 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
+
+ def self.stripe_secret_key
+ Danbooru.config.stripe_secret_key
+ end
+
+ 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
+ 2 * gold_price
+ end
+
+ def self.gold_to_platinum_price
+ platinum_price - gold_price
+ end
+
+ def level
+ case upgrade_type
+ when "gold"
+ User::Levels::GOLD
+ when "platinum"
+ User::Levels::PLATINUM
+ when "gold_to_platinum"
+ User::Levels::PLATINUM
+ else
+ raise NotImplementedError
+ end
+ end
+
+ def previous_level
+ case upgrade_type
+ when "gold"
+ User::Levels::MEMBER
+ when "platinum"
+ User::Levels::MEMBER
+ when "gold_to_platinum"
+ User::Levels::GOLD
+ else
+ raise NotImplementedError
+ end
+ end
+
+ def level_string
+ User.level_string(level)
+ end
+
+ def is_gift?
+ 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
+ return unless pending? || processing?
+
+ if payment_status == "paid"
+ upgrade_recipient!
+ create_mod_action!
+ dmail_recipient!
+ dmail_purchaser!
+ update!(status: :complete)
+ else
+ update!(status: :processing)
+ end
+ end
+ end
+
+ 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
+ def create_checkout!(country: "US", allow_promotion_codes: false)
+ methods = payment_method_types(country)
+ currency = preferred_currency(country)
+ price_id = upgrade_price_id(currency)
+
+ checkout = Stripe::Checkout::Session.create(
+ mode: "payment",
+ 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: purchaser.email_address&.address,
+ payment_method_types: methods,
+ allow_promotion_codes: allow_promotion_codes,
+ line_items: [{
+ price: price_id,
+ quantity: 1,
+ }],
+ metadata: {
+ user_upgrade_id: id,
+ purchaser_id: purchaser.id,
+ recipient_id: recipient.id,
+ purchaser_name: purchaser.name,
+ recipient_name: recipient.name,
+ upgrade_type: upgrade_type,
+ country: country,
+ is_gift: is_gift?,
+ level: level,
+ },
+ )
+
+ update!(stripe_id: checkout.id)
+ checkout
+ end
+
+ def refund!(reason: nil)
+ with_lock do
+ return if refunded?
+
+ Stripe::Refund.create(payment_intent: payment_intent.id, reason: reason)
+ recipient.update!(level: previous_level)
+ update!(status: "refunded")
+ end
+ end
+
+ def receipt_url
+ return nil if pending? || stripe_id.nil?
+ charge.receipt_url
+ end
+
+ def payment_url
+ return nil if pending? || stripe_id.nil?
+ "https://dashboard.stripe.com/payments/#{payment_intent.id}"
+ end
+
+ def checkout_session
+ @checkout_session ||= Stripe::Checkout::Session.retrieve(stripe_id)
+ end
+
+ def payment_intent
+ @payment_intent ||= Stripe::PaymentIntent.retrieve(checkout_session.payment_intent)
+ end
+
+ def charge
+ payment_intent.charges.data.first
+ end
+
+ def has_receipt?
+ !pending?
+ end
+
+ def has_payment?
+ !pending?
+ end
+
+ def upgrade_price_id(currency)
+ case [upgrade_type, currency]
+ when ["gold", "usd"]
+ Danbooru.config.stripe_gold_usd_price_id
+ when ["gold", "eur"]
+ Danbooru.config.stripe_gold_eur_price_id
+ when ["platinum", "usd"]
+ Danbooru.config.stripe_platinum_usd_price_id
+ when ["platinum", "eur"]
+ Danbooru.config.stripe_platinum_eur_price_id
+ when ["gold_to_platinum", "usd"]
+ Danbooru.config.stripe_gold_to_platinum_usd_price_id
+ when ["gold_to_platinum", "eur"]
+ Danbooru.config.stripe_gold_to_platinum_eur_price_id
+ else
+ raise NotImplementedError
+ end
+ end
+
+ def payment_method_types(country)
+ case country.to_s.upcase
+ # Austria, https://stripe.com/docs/payments/bancontact
+ when "AT"
+ ["card", "eps"]
+ # Belgium, https://stripe.com/docs/payments/eps
+ when "BE"
+ ["card", "bancontact"]
+ # Germany, https://stripe.com/docs/payments/giropay
+ when "DE"
+ ["card", "giropay"]
+ # Netherlands, https://stripe.com/docs/payments/ideal
+ when "NL"
+ ["card", "ideal"]
+ # Poland, https://stripe.com/docs/payments/p24
+ when "PL"
+ ["card", "p24"]
+ else
+ ["card"]
+ end
+ end
+
+ def preferred_currency(country)
+ case country.to_s.upcase
+ # Austria, Belgium, Germany, Netherlands, Poland
+ when "AT", "BE", "DE", "NL", "PL"
+ "eur"
+ else
+ "usd"
+ end
+ 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.data.object)
+ 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(checkout)
+ user_upgrade = UserUpgrade.find(checkout.metadata.user_upgrade_id)
+ user_upgrade.process_upgrade!(checkout.payment_status)
+ end
+ end
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e10c1312f..a38f782d0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -3,14 +3,17 @@ 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_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? }
+
+ 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
+ validate :validate_other_names
array_attribute :other_names
has_one :tag, :foreign_key => "name", :primary_key => "title"
@@ -33,6 +36,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)
@@ -41,7 +48,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)
@@ -61,9 +68,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, :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?
@@ -112,13 +117,19 @@ 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!
+ 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
+
+ def validate_other_names
+ if other_names.present? && tag&.artist?
+ errors.add(:base, "An artist wiki can't have other names")
end
end
@@ -139,12 +150,7 @@ class WikiPage < ApplicationRecord
end
def self.normalize_title(title)
- return if title.blank?
- title.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
- end
-
- def normalize_title
- self.title = WikiPage.normalize_title(title)
+ title.to_s.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
end
def normalize_other_names
@@ -244,10 +250,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 b0687b191..1f405d4f1 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, :updater, :wiki_page, :artist, :tag)
q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:body_matches])
@@ -77,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
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/app/policies/email_address_policy.rb b/app/policies/email_address_policy.rb
index cd92232d1..f9ddcdfdb 100644
--- a/app/policies/email_address_policy.rb
+++ b/app/policies/email_address_policy.rb
@@ -1,6 +1,10 @@
class EmailAddressPolicy < ApplicationPolicy
+ def index?
+ user.is_moderator?
+ end
+
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/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/app/policies/post_policy.rb b/app/policies/post_policy.rb
index 316e7034b..3bfa5d05c 100644
--- a/app/policies/post_policy.rb
+++ b/app/policies/post_policy.rb
@@ -88,11 +88,11 @@ class PostPolicy < ApplicationPolicy
def api_attributes
attributes = super
- attributes += [:has_large, :has_visible_children, :is_favorited?]
+ attributes += [:has_large, :has_visible_children]
attributes += TagCategory.categories.map {|x| "tag_string_#{x}".to_sym}
attributes += [:file_url, :large_file_url, :preview_file_url] if visible?
attributes -= [:id, :md5, :file_ext] if !visible?
- attributes -= [:fav_string] if !user.is_moderator?
+ attributes -= [:fav_string]
attributes
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/policies/user_policy.rb b/app/policies/user_policy.rb
index 0c9c0469c..1aaaaa84f 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -1,6 +1,10 @@
class UserPolicy < ApplicationPolicy
def create?
- user.is_anonymous? && !sockpuppet?
+ true
+ end
+
+ def new?
+ true
end
def update?
@@ -23,12 +27,12 @@ class UserPolicy < ApplicationPolicy
user.is_member?
end
- def can_see_favorites?
- user.is_admin? || record.id == user.id || !record.enable_private_favorites?
+ def can_see_last_logged_in_at?
+ user.is_moderator?
end
- def sockpuppet?
- User.where(last_ip_addr: request.remote_ip).where("created_at > ?", 1.day.ago).exists?
+ def can_see_favorites?
+ user.is_admin? || record.id == user.id || !record.enable_private_favorites?
end
def permitted_attributes_for_create
@@ -47,7 +51,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/app/policies/user_upgrade_policy.rb b/app/policies/user_upgrade_policy.rb
new file mode 100644
index 000000000..d5022a350
--- /dev/null
+++ b/app/policies/user_upgrade_policy.rb
@@ -0,0 +1,25 @@
+class UserUpgradePolicy < ApplicationPolicy
+ def create?
+ user.is_member?
+ end
+
+ def new?
+ UserUpgrade.enabled?
+ end
+
+ def show?
+ record.recipient == user || record.purchaser == user || user.is_owner?
+ end
+
+ def refund?
+ user.is_owner? && record.complete?
+ end
+
+ def receipt?
+ (record.purchaser == user || user.is_owner?) && record.has_receipt?
+ end
+
+ def payment?
+ user.is_owner? && record.has_payment?
+ end
+end
diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb
index 60d6ba4b1..6135e1020 100644
--- a/app/presenters/post_presenter.rb
+++ b/app/presenters/post_presenter.rb
@@ -131,7 +131,6 @@ class PostPresenter
"data-source" => post.source,
"data-uploader-id" => post.uploader_id,
"data-normalized-source" => post.normalized_source,
- "data-is-favorited" => post.favorited_by?(CurrentUser.user.id)
}
if post.visible?
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| %>
diff --git a/app/views/emails/index.html.erb b/app/views/emails/index.html.erb
new file mode 100644
index 000000000..8fc580d14
--- /dev/null
+++ b/app/views/emails/index.html.erb
@@ -0,0 +1,46 @@
+
+
+ <%= 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_ilike] }, hint: "Use * for wildcard" %>
+ <%= f.input :normalized_address_ilike, label: "Normalized Address", input_html: { value: params[:search][:normalized_address_ilike] }, hint: "Use * for wildcard" %>
+ <%= f.input :is_verified, label: "Verified?", as: :select, include_blank: true, selected: params[:search][:is_verified] %>
+ <%= f.input :is_restricted, label: "Restricted?", as: :select, include_blank: true, selected: params[:search][:is_restricted] %>
+ <%= 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 do |email| %>
+ <%= link_to email.address, emails_path(search: { address_ilike: email.address }) %>
+ <% end %>
+ <% t.column :normalized_address do |email| %>
+ <% unless email.is_normalized? %>
+ <%= link_to email.normalized_address, emails_path(search: { normalized_address_ilike: email.normalized_address }) %>
+ <% end %>
+ <% end %>
+ <% t.column "Restricted?" do |email| %>
+ <% if email.is_restricted? %>
+ <%= link_to "Yes", emails_path(search: { is_restricted: true }) %>
+ <% end %>
+ <% end %>
+ <% t.column "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/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/explore/posts/missed_searches.html.erb b/app/views/explore/posts/missed_searches.html.erb
index 329b66c43..1bad6c802 100644
--- a/app/views/explore/posts/missed_searches.html.erb
+++ b/app/views/explore/posts/missed_searches.html.erb
@@ -15,11 +15,11 @@
- <% @missed_searches.each do |tags, count| %>
-
- <%= link_to tags, posts_path(:tags => tags) %>
+ <% @missed_searches.each do |search, count| %>
+
+ <%= link_to search, posts_path(tags: search) %>
- <% unless WikiPage.titled(tags).exists? %>
+ <% unless WikiPage.titled(search).exists? %>
N
<% end %>
diff --git a/app/views/explore/posts/searches.html.erb b/app/views/explore/posts/searches.html.erb
index 5256427eb..a00154e61 100644
--- a/app/views/explore/posts/searches.html.erb
+++ b/app/views/explore/posts/searches.html.erb
@@ -13,9 +13,9 @@
- <% @searches.each do |tags, count| %>
-
- <%= link_to tags, posts_path(:tags => tags) %>
+ <% @searches.each do |search, count| %>
+
+ <%= link_to search, posts_path(tags: search) %>
<%= count.to_i %>
<% end %>
diff --git a/app/views/favorites/_update.js.erb b/app/views/favorites/_update.js.erb
index 73b71ec61..0c2ece4aa 100644
--- a/app/views/favorites/_update.js.erb
+++ b/app/views/favorites/_update.js.erb
@@ -14,7 +14,7 @@ $(".fav-buttons").toggleClass("fav-buttons-false").toggleClass("fav-buttons-true
<% if policy(@post).can_view_favlist? %>
var fav_count = <%= @post.fav_count %>;
- $("#favlist").html("<%= j post_favlist(@post) %>");
+ $("#favlist").html("<%= j render "posts/partials/show/favorite_list", post: @post %>");
if (fav_count === 0) {
$("#show-favlist-link, #hide-favlist-link, #favlist").hide();
diff --git a/app/views/ip_bans/index.html.erb b/app/views/ip_bans/index.html.erb
index 5a68e17f9..7313ec31c 100644
--- a/app/views/ip_bans/index.html.erb
+++ b/app/views/ip_bans/index.html.erb
@@ -2,9 +2,19 @@
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: "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 %>
+
<%= 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| %>
@@ -25,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 %>
diff --git a/app/views/layouts/_main_links.html.erb b/app/views/layouts/_main_links.html.erb
index 75d515e36..6ec3071b5 100644
--- a/app/views/layouts/_main_links.html.erb
+++ b/app/views/layouts/_main_links.html.erb
@@ -1,6 +1,6 @@
<% if CurrentUser.is_anonymous? %>
- <%= nav_link_to("Login", login_path(url: request.fullpath)) %>
+ <%= nav_link_to("Login", login_path(url: request.fullpath), rel: "nofollow") %>
<% else %>
<%= nav_link_to("My Account #{unread_dmail_indicator(CurrentUser.user)}", profile_path) %>
<% end %>
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? %>
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/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/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/app/views/posts/partials/index/_preview.html.erb b/app/views/posts/partials/index/_preview.html.erb
index a9f00ebb1..0e03f2613 100644
--- a/app/views/posts/partials/index/_preview.html.erb
+++ b/app/views/posts/partials/index/_preview.html.erb
@@ -38,11 +38,7 @@
<%= link_to recommended_posts_path(search: { post_id: post.id }), class: "more-recommended-posts", "data-post-id": post.id do %>
<%= post.fav_count %>
- <% if post.favorited_by?(CurrentUser.id) %>
-
- <% else %>
-
- <% end %>
+
more »
<% end %>
diff --git a/app/views/posts/partials/show/_edit.html.erb b/app/views/posts/partials/show/_edit.html.erb
index 1c47dbc96..f7ee181b2 100644
--- a/app/views/posts/partials/show/_edit.html.erb
+++ b/app/views/posts/partials/show/_edit.html.erb
@@ -54,7 +54,7 @@
- <%= f.input :tag_string, label: false, input_html: { size: "60x5", spellcheck: false, "data-autocomplete": "tag-edit", "data-shortcut": "e", value: post.presenter.split_tag_list_text + " " } %>
+ <%= f.input :tag_string, label: false, input_html: { size: "60x5", "data-autocomplete": "tag-edit", "data-shortcut": "e", value: post.presenter.split_tag_list_text + " " } %>
<%= render "related_tags/buttons" %>
diff --git a/app/views/posts/partials/show/_favorite_list.html.erb b/app/views/posts/partials/show/_favorite_list.html.erb
new file mode 100644
index 000000000..863a24a7a
--- /dev/null
+++ b/app/views/posts/partials/show/_favorite_list.html.erb
@@ -0,0 +1,2 @@
+<%# post %>
+<%= safe_join(post.visible_favorited_users(CurrentUser.user).map { |user| link_to_user(user) }, ", ") %>
diff --git a/app/views/posts/partials/show/_information.html.erb b/app/views/posts/partials/show/_information.html.erb
index dcfc2b11c..51ce633b2 100644
--- a/app/views/posts/partials/show/_information.html.erb
+++ b/app/views/posts/partials/show/_information.html.erb
@@ -37,7 +37,9 @@
<% if policy(post).can_view_favlist? %>
<%= link_to "Show »", "#", id: "show-favlist-link", style: ("display: none;" if post.fav_count == 0) %>
<%= link_to "« Hide", "#", id: "hide-favlist-link", style: "display: none;" %>
- <%= post_favlist(post) %>
+
+ <%= render "posts/partials/show/favorite_list", post: post %>
+
<% end %>
Status:
diff --git a/app/views/posts/partials/show/_options.html.erb b/app/views/posts/partials/show/_options.html.erb
index d9d2949c8..7190bdf4c 100644
--- a/app/views/posts/partials/show/_options.html.erb
+++ b/app/views/posts/partials/show/_options.html.erb
@@ -23,10 +23,10 @@
<% if policy(Favorite).create? %>
- <%= link_to "Favorite", favorites_path(post_id: post.id), remote: true, method: :post, id: "add-to-favorites", "data-shortcut": "f", style: ("display: none;" if @post.is_favorited?) %>
+ <%= link_to "Favorite", favorites_path(post_id: post.id), remote: true, method: :post, id: "add-to-favorites", "data-shortcut": "f", style: ("display: none;" if @post.favorited_by?(CurrentUser.user)) %>
- <%= link_to "Unfavorite", favorite_path(post), remote: true, method: :delete, id: "remove-from-favorites", "data-shortcut": "shift+f", "data-shortcut-when": ":visible", style: ("display: none;" if !@post.is_favorited?) %>
+ <%= link_to "Unfavorite", favorite_path(post), remote: true, method: :delete, id: "remove-from-favorites", "data-shortcut": "shift+f", "data-shortcut-when": ":visible", style: ("display: none;" if !@post.favorited_by?(CurrentUser.user)) %>
<% end %>
<% if policy(post).update? %>
diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb
index b035b389c..a2f202f27 100644
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -50,7 +50,7 @@
<% end -%>
<% if policy(Favorite).create? %>
- <%= content_tag(:div, class: "fav-buttons fav-buttons-#{@post.is_favorited?}") do %>
+ <%= content_tag(:div, class: "fav-buttons fav-buttons-#{@post.favorited_by?(CurrentUser.user)}") do %>
<%= form_tag(favorites_path(post_id: @post.id), method: "post", id: "add-fav-button", "data-remote": true) do %>
<%= button_tag tag.i(class: "far fa-heart"), class: "ui-button ui-widget ui-corner-all", "data-disable-with": tag.i(class: "fas fa-spinner fa-spin") %>
<% end %>
diff --git a/app/views/robots/index.text.erb b/app/views/robots/index.text.erb
index 57244f162..ca2007b76 100644
--- a/app/views/robots/index.text.erb
+++ b/app/views/robots/index.text.erb
@@ -6,25 +6,29 @@ Allow: /$
Disallow: /*.atom
Disallow: /*.json
-Allow: /artists
-Allow: /artist_commentaries
-Allow: /comments
-Allow: /explore
-Allow: /favorite_groups
-Allow: /forum_posts
-Allow: /forum_topics
-Allow: /iqdb_queries
-Allow: /login
-Allow: /notes
-Allow: /pools
-Allow: /posts
-Allow: /sessions
-Allow: /static
-Allow: /tags
-Allow: /uploads
-Allow: /user_upgrade
-Allow: /users
-Allow: /wiki_pages
+Allow: <%= artists_path %>
+Allow: <%= artist_commentaries_path %>
+Allow: <%= comments_path %>
+Allow: <%= popular_explore_posts_path %>
+Allow: <%= curated_explore_posts_path %>
+Allow: <%= viewed_explore_posts_path %>
+Allow: <%= searches_explore_posts_path %>
+Allow: <%= missed_searches_explore_posts_path %>
+Allow: <%= favorite_groups_path %>
+Allow: <%= forum_posts_path %>
+Allow: <%= forum_topics_path %>
+Allow: <%= iqdb_queries_path %>
+Allow: <%= login_path %>
+Allow: <%= notes_path %>
+Allow: <%= pools_path %>
+Allow: <%= posts_path %>
+Allow: <%= new_session_path %>
+Allow: <%= sign_out_session_path %>
+Allow: <%= tags_path %>
+Allow: <%= uploads_path %>
+Allow: <%= user_upgrades_path %>
+Allow: <%= users_path %>
+Allow: <%= wiki_pages_path %>
<%# Legacy redirects %>
Allow: /artist
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
index 7ce1ffa31..c9f748da0 100644
--- a/app/views/sessions/new.html.erb
+++ b/app/views/sessions/new.html.erb
@@ -1,5 +1,6 @@
<% page_title "Login" %>
<% meta_description "Login to #{Danbooru.config.app_name}" %>
+<% canonical_url login_url %>
<%= render "sessions/secondary_links" %>
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/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/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/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index b88b8c138..d6a93fdfe 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) %>
@@ -134,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) %>
@@ -154,6 +152,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/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 %>
+
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/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/app/views/uploads/new.html.erb b/app/views/uploads/new.html.erb
index 51552cedb..a41132c1e 100644
--- a/app/views/uploads/new.html.erb
+++ b/app/views/uploads/new.html.erb
@@ -69,7 +69,7 @@
- <%= f.input :tag_string, label: false, input_html: { size: "60x5", "data-autocomplete": "tag-edit", "data-shortcut": "e", spellcheck: false, value: params[:tag_string] } %>
+ <%= f.input :tag_string, label: false, input_html: { size: "60x5", "data-autocomplete": "tag-edit", "data-shortcut": "e", value: params[:tag_string] } %>
<%= render "related_tags/buttons" %>
diff --git a/app/views/user_upgrades/_stripe_links.html.erb b/app/views/user_upgrades/_stripe_links.html.erb
new file mode 100644
index 000000000..7aa57743d
--- /dev/null
+++ b/app/views/user_upgrades/_stripe_links.html.erb
@@ -0,0 +1,9 @@
+<%# user_upgrade %>
+
+<% if policy(user_upgrade).receipt? %>
+ <%= link_to "View Receipt", receipt_user_upgrade_path(user_upgrade), target: "_blank" %>
+<% end %>
+
+<% if policy(user_upgrade).payment? %>
+ | <%= link_to "View Payment", payment_user_upgrade_path(user_upgrade), target: "_blank" %>
+<% 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/index.html.erb b/app/views/user_upgrades/index.html.erb
new file mode 100644
index 000000000..2a0448406
--- /dev/null
+++ b/app/views/user_upgrades/index.html.erb
@@ -0,0 +1,51 @@
+
+
+ <%= 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 |user_upgrade| %>
+ <%= time_ago_in_words_tagged(user_upgrade.updated_at) %>
+ <% end %>
+
+ <% t.column column: "control" do |user_upgrade| %>
+ <%= link_to "Show", user_upgrade %>
+ <% if policy(user_upgrade).receipt? %>
+ | <%= link_to "Receipt", receipt_user_upgrade_path(user_upgrade), target: "_blank" %>
+ <% end %>
+ <% if policy(user_upgrade).payment? %>
+ | <%= link_to "Payment", payment_user_upgrade_path(user_upgrade), target: "_blank" %>
+ <% end %>
+ <% if policy(user_upgrade).refund? %>
+ | <%= link_to "Refund", refund_user_upgrade_path(user_upgrade), remote: true, method: :put, "data-confirm": "Are you sure you want to refund this payment?" %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <%= numbered_paginator(@user_upgrades) %>
+
+
diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb
index 4cd610bb3..7e25103b7 100644
--- a/app/views/user_upgrades/new.html.erb
+++ b/app/views/user_upgrades/new.html.erb
@@ -1,105 +1,173 @@
<% 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" %>
-
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 %>
+
+
+
+
+ Basic
+ Gold
+ Platinum
+
+
+
+
+
+
+
+
+
+ Free
+
+ <%= cents_to_usd(UserUpgrade.gold_price) %>
+ One time fee
+
+
+ <%= cents_to_usd(UserUpgrade.platinum_price) %>
+ One time fee
+
+
+
+ Tag Limit
+ <%= User.tag_query_limit(User::Levels::MEMBER) %>
+ <%= User.tag_query_limit(User::Levels::GOLD) %>
+ <%= User.tag_query_limit(User::Levels::PLATINUM) %>
+
+
+ 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", country: params[:country], promo: params[:promo]), remote: true, disable_with: "Redirecting..." %>
+ <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "platinum", country: params[:country], promo: params[:promo]), 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", country: params[:country], promo: params[:promo]), 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 %>
+
+
+
+
+
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. We also support bank payments in several European countries,
+ including Austria, Belgium, Germany, the Netherlands, and Poland.
+
+ 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.
+
-
- <% 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? %>
- <%= render "stripe_payment" %>
- <% else %>
- <%= render "stripe_payment_safebooru" %>
- <% end %>
- <% end %>
diff --git a/app/views/user_upgrades/refund.js.erb b/app/views/user_upgrades/refund.js.erb
new file mode 100644
index 000000000..345366b9b
--- /dev/null
+++ b/app/views/user_upgrades/refund.js.erb
@@ -0,0 +1 @@
+location.reload();
diff --git a/app/views/user_upgrades/show.html.erb b/app/views/user_upgrades/show.html.erb
index 014421551..a81978fc5 100644
--- a/app/views/user_upgrades/show.html.erb
+++ b/app/views/user_upgrades/show.html.erb
@@ -1,22 +1,56 @@
-<% 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
+ by <%= link_to_user @user_upgrade.purchaser %>
+ <% if @user_upgrade.is_gift? %>
+ for <%= link_to_user @user_upgrade.recipient %>
+ <% end %>
+
+
+ Updated
+ <%= time_ago_in_words_tagged @user_upgrade.updated_at %>
+
+
+ Upgrade Type
+ <%= @user_upgrade.upgrade_type.humanize %>
+
+
+ Status
+ <%= @user_upgrade.status.humanize %>
+
+
+
+
+ <% if @user_upgrade.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! A receipt has been sent to your email.
<% 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! A receipt has been sent to your email.
<% 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 %>
+ <%= render "stripe_links", user_upgrade: @user_upgrade %>
+ <% elsif @user_upgrade.refunded? %>
+
This purchase has been refunded. A receipt has been sent to your email. It can take up to
+ 5-10 days for the refund to appear on your credit card or bank statement. If it takes longer,
+ please contact your bank for assistance.
+
+ <%= render "stripe_links", user_upgrade: @user_upgrade %>
+ <% 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/app/views/users/_post_summary.html.erb b/app/views/users/_post_summary.html.erb
index 208a237ca..b8987744e 100644
--- a/app/views/users/_post_summary.html.erb
+++ b/app/views/users/_post_summary.html.erb
@@ -18,7 +18,7 @@
<% presenter.favorites.each do |post| %>
- <%= PostPresenter.preview(post, :tags => "fav:#{user.name}") %>
+ <%= PostPresenter.preview(post, tags: "ordfav:#{user.name}") %>
<% end %>
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/_statistics.html.erb b/app/views/users/_statistics.html.erb
index 884a4c100..581028cbd 100644
--- a/app/views/users/_statistics.html.erb
+++ b/app/views/users/_statistics.html.erb
@@ -10,6 +10,14 @@
Join Date
<%= 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
@@ -27,6 +35,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 %>
@@ -40,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/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 2c3bae957..18dfbc6c2 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -2,40 +2,54 @@
Users
- <%= 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| %>
+ <%= 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%", 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| %>
+ <% if user.last_ip_addr.present? %>
+ <%= link_to user.last_ip_addr, ip_address_path(user.last_ip_addr) %>
+ <% end %>
<% 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 %>
+ <% 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 %>
+ <% 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) %>
-
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/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/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" %>
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/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/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/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 0d2cc3321..ed77e0ae9 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -46,6 +46,21 @@ 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
+
+ # 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
@@ -54,6 +69,19 @@ module Danbooru
"#{source_code_url}/issues"
end
+ # If true, new accounts will require email verification if they seem
+ # suspicious (they were created using a proxy, multiple accounts were
+ # created by the same IP, etc).
+ #
+ # This doesn't apply to personal or development installs running on
+ # localhost or the local network.
+ #
+ # Disable this if you're running a public booru and you don't want email
+ # verification for new accounts.
+ def new_user_verification?
+ true
+ end
+
# An array of regexes containing disallowed usernames.
def user_name_blacklist
[]
@@ -79,11 +107,6 @@ module Danbooru
2
end
- # Users cannot search for more than X regular tags at a time.
- def base_tag_query_limit
- 6
- end
-
# After this many pages, the paginator will switch to sequential mode.
def max_numbered_pages
1_000
@@ -266,6 +289,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
[]
@@ -339,6 +367,30 @@ module Danbooru
def stripe_publishable_key
end
+ def stripe_webhook_secret
+ end
+
+ def stripe_gold_usd_price_id
+ end
+
+ def stripe_platinum_usd_price_id
+ end
+
+ def stripe_gold_to_platinum_usd_price_id
+ end
+
+ def stripe_gold_eur_price_id
+ end
+
+ def stripe_platinum_eur_price_id
+ end
+
+ def stripe_gold_to_platinum_eur_price_id
+ end
+
+ def stripe_promotion_discount_id
+ end
+
def twitter_api_key
end
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/core_extensions.rb b/config/initializers/core_extensions.rb
index 61f85f9d0..2456c6b9a 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
@@ -36,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/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/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?
+)
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/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
diff --git a/config/routes.rb b/config/routes.rb
index ce3a273c1..13e8a809c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,14 @@
Rails.application.routes.draw do
+ 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
+ # 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 +77,6 @@ Rails.application.routes.draw do
get :search
end
end
- resources :autocomplete, only: [:index]
resources :bans
resources :bulk_update_requests do
member do
@@ -103,6 +112,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
@@ -141,9 +151,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
@@ -173,7 +180,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
@@ -181,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
@@ -217,20 +223,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]
- resources :tags do
- collection do
- get :autocomplete
- end
- end
+ resource :status, only: [:show], controller: "status"
+ resources :tags
resources :tag_aliases, only: [:show, :index, :destroy]
resources :tag_implications, only: [:show, :index, :destroy]
resources :uploads do
@@ -252,13 +251,19 @@ Rails.application.routes.draw do
end
collection do
- get :search
get :custom_style
end
end
- resource :user_upgrade, :only => [:new, :create, :show]
+ resources :user_upgrades, only: [:new, :create, :show, :index] do
+ get :receipt, on: :member
+ get :payment, on: :member
+ put :refund, on: :member
+ end
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
@@ -270,100 +275,40 @@ Rails.application.routes.draw do
end
end
- # 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)}"}
- 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]}"}
-
+ # 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}")
+ get "/artist/show" => redirect {|params, req| "/artists?name=#{CGI.escape(req.params[:name].to_s)}"}
+
+ get "/forum" => 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 "/pool/show/:id" => redirect("/pools/%{id}")
+
+ get "/post/index" => redirect {|params, req| "/posts?tags=#{CGI.escape(req.params[:tags].to_s)}&page=#{req.params[:page]}"}
+ get "/post/atom" => redirect {|params, req| "/posts.atom?tags=#{CGI.escape(req.params[:tags].to_s)}"}
+ get "/post/show/:id/:tag_title" => redirect("/posts/%{id}")
+ get "/post/show/:id" => redirect("/posts/%{id}")
+
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/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}")
- 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])}"}
+
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"
@@ -373,7 +318,8 @@ Rails.application.routes.draw do
get "/static/site_map" => "static#site_map", :as => "site_map"
get "/static/contact" => "static#contact", :as => "contact"
get "/static/dtext_help" => "static#dtext_help", :as => "dtext_help"
- get "/static/terms_of_service" => redirect { "/terms_of_service" }
+ get "/static/terms_of_service", to: redirect("/terms_of_service")
+ get "/user_upgrade/new", to: redirect("/user_upgrades/new")
get "/mock/recommender/recommend/:user_id" => "mock_services#recommender_recommend", as: "mock_recommender_recommend"
get "/mock/recommender/similiar/:post_id" => "mock_services#recommender_similar", as: "mock_recommender_similar"
@@ -383,7 +329,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"
+ match "*other", to: "static#not_found", via: :all
end
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/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/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/populate.rb b/db/populate.rb
index c6451e7b7..76a517ed3 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(
@@ -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|
diff --git a/db/structure.sql b/db/structure.sql
index 7da011e05..f57281afa 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: -
--
@@ -2721,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,
@@ -3089,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: -
--
@@ -4157,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: -
--
@@ -4522,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: -
--
@@ -7030,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: -
--
@@ -7420,6 +7520,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200520060951'),
('20200803022359'),
('20200816175151'),
-('20201201211748');
+('20201201211748'),
+('20201213052805'),
+('20201219201007'),
+('20201224101208');
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/public/fonts/gisele.ttf b/public/fonts/gisele.ttf
new file mode 100644
index 000000000..d04d1c330
Binary files /dev/null and b/public/fonts/gisele.ttf differ
diff --git a/public/images/ablobgift.gif b/public/images/ablobgift.gif
new file mode 100644
index 000000000..bca83dc11
Binary files /dev/null and b/public/images/ablobgift.gif differ
diff --git a/script/fixes/067_delete_invalid_email_addresses.rb b/script/fixes/067_delete_invalid_email_addresses.rb
new file mode 100755
index 000000000..349ce24cd
--- /dev/null
+++ b/script/fixes/067_delete_invalid_email_addresses.rb
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require_relative "../../config/environment"
+
+EmailAddress.transaction do
+ EmailAddress.valid(false).destroy_all
+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/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/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/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/functional/autocomplete_controller_test.rb b/test/functional/autocomplete_controller_test.rb
index 51c064029..fa877579d 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,35 @@ 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
+
+ should "work for a missing type" do
+ get autocomplete_index_path(search: { query: "azur" }), as: :json
+
+ assert_response :success
+ assert_equal([], response.parsed_body)
+ 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
diff --git a/test/functional/bulk_update_requests_controller_test.rb b/test/functional/bulk_update_requests_controller_test.rb
index b72db21bc..0864f8a54 100644
--- a/test/functional/bulk_update_requests_controller_test.rb
+++ b/test/functional/bulk_update_requests_controller_test.rb
@@ -139,6 +139,17 @@ class BulkUpdateRequestsControllerTest < ActionDispatch::IntegrationTest
end
context "for a builder" do
+ should "fail when moving a non-artist tag" do
+ create(:tag, name: "sfw", post_count: 0)
+ @bulk_update_request = create(:bulk_update_request, script: "alias sfw -> rating:s")
+
+ post_auth approve_bulk_update_request_path(@bulk_update_request), @builder
+
+ assert_response 403
+ assert_equal("pending", @bulk_update_request.reload.status)
+ assert_equal(false, TagAlias.exists?(antecedent_name: "sfw", consequent_name: "rating:s"))
+ end
+
should "fail for a large artist move" do
create(:tag, name: "artist1", category: Tag.categories.artist, post_count: 1000)
@bulk_update_request = create(:bulk_update_request, script: "create alias artist1 -> artist2")
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
diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb
index ec08a2c03..8d646a382 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
@@ -68,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
@@ -81,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/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 }
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
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/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" } }
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/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb
index 18ad1ba37..af4c0b2e2 100644
--- a/test/functional/posts_controller_test.rb
+++ b/test/functional/posts_controller_test.rb
@@ -633,7 +633,6 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_nil(response.parsed_body["md5"])
assert_nil(response.parsed_body["file_url"])
- assert_nil(response.parsed_body["fav_string"])
end
end
end
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/static_controller_test.rb b/test/functional/static_controller_test.rb
index faca87b71..13b99b53c 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
@@ -62,6 +94,8 @@ class StaticControllerTest < ActionDispatch::IntegrationTest
context "contact action" do
should "work" do
+ create(:owner_user)
+
get contact_path
assert_response :success
end
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
diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb
index 56dead38c..f00bfb7ab 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] }
@@ -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/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb
index b4450f8a1..9dc90da51 100644
--- a/test/functional/user_upgrades_controller_test.rb
+++ b/test/functional/user_upgrades_controller_test.rb
@@ -3,16 +3,362 @@ 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 to Gold" do
+ @user = create(:user)
+ get_auth new_user_upgrade_path, @user
+
+ assert_response :success
+ end
+
+ 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 the country param" do
+ get new_user_upgrade_path(country: "DE")
+
+ assert_response :success
+ end
+
+ should "render for the promo param" do
+ get new_user_upgrade_path(promo: "true")
+
+ 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)
+ 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
+ @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 refunded upgrade" do
+ should "render" do
+ @user_upgrade = create(:self_gold_upgrade, status: "refunded")
+ get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response :success
+ 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 "receipt action" do
+ mock_stripe!
+
+ setup do
+ @user_upgrade = create(:gift_gold_upgrade, status: "complete")
+ @user_upgrade.create_checkout!
+ end
+
+ should "not allow unauthorized users to view the receipt" do
+ get_auth receipt_user_upgrade_path(@user_upgrade), create(:user)
+
+ assert_response 403
+ end
+
+ should "not allow the recipient to view the receipt" do
+ get_auth receipt_user_upgrade_path(@user_upgrade), @user_upgrade.recipient
+
+ assert_response 403
+ end
+
+ should "not allow the purchaser to view a pending receipt" do
+ @user_upgrade.update!(status: "pending")
+ get_auth receipt_user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response 403
+ end
+
+ # XXX not supported yet by stripe-ruby-mock
+ should_eventually "allow the purchaser to view the receipt" do
+ get_auth receipt_user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_redirected_to "xxx"
+ end
+
+ # XXX not supported yet by stripe-ruby-mock
+ should_eventually "allow the site owner to view the receipt" do
+ get_auth receipt_user_upgrade_path(@user_upgrade), create(:owner_user)
+
+ assert_redirected_to "xxx"
+ end
+ end
+
+ context "payment action" do
+ setup do
+ @user_upgrade = create(:gift_gold_upgrade, status: "complete")
+ @user_upgrade.create_checkout!
+ end
+
+ should "not allow unauthorized users to view the receipt" do
+ get_auth payment_user_upgrade_path(@user_upgrade), @user_upgrade.purchaser
+
+ assert_response 403
+ end
+
+ # XXX not supported yet by stripe-ruby-mock
+ should_eventually "allow the site owner to view the receipt" do
+ get_auth payment_user_upgrade_path(@user_upgrade), create(:owner_user)
+
+ assert_redirected_to "xxx"
+ end
+ end
+
+ context "refund action" do
+ mock_stripe!
+
+ context "for a self upgrade" do
+ context "to Gold" do
+ should_eventually "refund the upgrade" do
+ @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete")
+ @user_upgrade.create_checkout!
+
+ put_auth refund_user_upgrade_path(@user_upgrade), create(:owner_user), xhr: true
+
+ assert_response :success
+ assert_equal("refunded", @user_upgrade.reload.status)
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level)
+ end
+ end
+ end
+
+ context "for a gifted upgrade" do
+ context "to Platinum" do
+ should_eventually "refund the upgrade" do
+ @user_upgrade = create(:gift_platinum_upgrade, recipient: create(:platinum_user), status: "complete")
+ @user_upgrade.create_checkout!
+
+ put_auth refund_user_upgrade_path(@user_upgrade), create(:owner_user), xhr: true
+
+ assert_response :success
+ assert_equal("refunded", @user_upgrade.reload.status)
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level)
+ end
+ end
+ end
+
+ should "not allow unauthorized users to create a refund" do
+ @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete")
+ @user_upgrade.create_checkout!
+
+ put_auth refund_user_upgrade_path(@user_upgrade), @user_upgrade.purchaser, xhr: true
+
+ assert_response 403
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.level)
+ end
+ end
+
+ context "create action" do
+ mock_stripe!
+
+ context "for a self upgrade" do
+ context "to Gold" 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" }, 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" do
+ context "to Gold" 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: "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
end
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index c6c02d522..85adf6a47 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)
@@ -216,6 +243,11 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
get new_user_path
assert_response :success
end
+
+ should "render for a logged in user" do
+ get_auth new_user_path, @user
+ assert_response :success
+ end
end
context "create action" do
@@ -229,11 +261,6 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_no_enqueued_emails
end
- should "not allow logged in users to create a new account" do
- post_auth users_path, @user, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
- assert_response 403
- end
-
should "create a user with a valid email" do
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1", email: "webmaster@danbooru.donmai.us" }}
@@ -241,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
@@ -262,45 +289,81 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end
end
- should "mark users signing up from proxies as requiring verification" do
- skip unless IpLookup.enabled?
+ context "sockpuppet detection" do
+ setup do
+ @private_ip = "192.168.0.1"
+ @valid_ip = "187.37.226.17" # a random valid, non-proxy public IP
+ @valid_ipv6 = "2600:1700:6b0:a518::1"
+ @proxy_ip = "51.15.128.1"
+ end
- self.remote_addr = "51.15.128.1"
- post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+ should "work for a public IPv6 address" do
+ self.remote_addr = @valid_ipv6
- assert_redirected_to User.last
- assert_equal(true, User.last.requires_verification)
- end
+ post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
- should "mark users signing up from a partial banned IP as requiring verification" do
- skip unless IpLookup.enabled?
- self.remote_addr = "187.37.226.17"
+ assert_redirected_to User.last
+ assert_equal(false, User.last.requires_verification)
+ end
- @ip_ban = create(:ip_ban, ip_addr: self.remote_addr, category: :partial)
- post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+ should "mark accounts created by already logged in users as requiring verification" do
+ self.remote_addr = @valid_ip
- assert_redirected_to User.last
- assert_equal(true, User.last.requires_verification)
- assert_equal(1, @ip_ban.reload.hit_count)
- assert(@ip_ban.last_hit_at > 1.minute.ago)
- end
+ post_auth users_path, @user, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
- should "not mark users signing up from non-proxies as requiring verification" do
- skip unless IpLookup.enabled?
- self.remote_addr = "187.37.226.17"
- post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+ assert_redirected_to User.last
+ assert_equal(true, User.last.requires_verification)
+ end
- assert_redirected_to User.last
- assert_equal(false, User.last.requires_verification)
- end
+ should "mark users signing up from proxies as requiring verification" do
+ skip unless IpLookup.enabled?
+ self.remote_addr = @proxy_ip
- context "with sockpuppet validation enabled" do
- should "not allow registering multiple accounts with the same IP" do
- assert_difference("User.count", 0) do
- @user.update(last_ip_addr: "127.0.0.1")
- post users_path, params: {:user => {:name => "dupe", :password => "xxxxx1", :password_confirmation => "xxxxx1"}}
- assert_response 403
- end
+ post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+
+ assert_redirected_to User.last
+ assert_equal(true, User.last.requires_verification)
+ end
+
+ should "mark users signing up from a partial banned IP as requiring verification" do
+ self.remote_addr = @valid_ip
+
+ @ip_ban = create(:ip_ban, ip_addr: self.remote_addr, category: :partial)
+ post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+
+ assert_redirected_to User.last
+ assert_equal(true, User.last.requires_verification)
+ assert_equal(1, @ip_ban.reload.hit_count)
+ assert(@ip_ban.last_hit_at > 1.minute.ago)
+ end
+
+ should "not mark users signing up from non-proxies as requiring verification" do
+ skip unless IpLookup.enabled?
+ self.remote_addr = @valid_ip
+
+ post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+
+ assert_redirected_to User.last
+ assert_equal(false, User.last.requires_verification)
+ end
+
+ should "mark accounts registered from an IPv4 address recently used for another account as requiring verification" do
+ @user.update!(last_ip_addr: @valid_ip)
+ self.remote_addr = @valid_ip
+
+ post users_path, params: { user: { name: "dupe", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+
+ assert_redirected_to User.last
+ assert_equal(true, User.last.requires_verification)
+ end
+
+ should "not mark users signing up from localhost as requiring verification" do
+ self.remote_addr = "127.0.0.1"
+
+ post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
+
+ assert_redirected_to User.last
+ assert_equal(false, User.last.requires_verification)
end
end
end
@@ -335,11 +398,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
diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb
new file mode 100644
index 000000000..3ac99ae1e
--- /dev/null
+++ b/test/functional/webhooks_controller_test.rb
@@ -0,0 +1,151 @@
+require 'test_helper'
+
+class WebhooksControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ StripeMock.start
+ end
+
+ teardown do
+ StripeMock.stop
+ end
+
+ 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 }
+
+ 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 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 "to Gold" do
+ should "upgrade the user" do
+ @user_upgrade = create(:self_gold_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level)
+ end
+ end
+
+ context "to Platinum" do
+ should "upgrade the user" do
+ @user_upgrade = create(:self_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
+ end
+ end
+
+ context "from Gold to Platinum" do
+ should "upgrade the user" do
+ @user_upgrade = create(:self_gold_to_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ 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 "to Gold" do
+ should "upgrade the user" do
+ @user_upgrade = create(:gift_gold_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level)
+ end
+ end
+
+ context "to Platinum" do
+ should "upgrade the user" do
+ @user_upgrade = create(:gift_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
+ end
+ end
+
+ context "from Gold to Platinum" do
+ should "upgrade the user" do
+ @user_upgrade = create(:gift_gold_to_platinum_upgrade)
+ post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id })
+
+ assert_response 200
+ assert_equal("complete", @user_upgrade.reload.status)
+ assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
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/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/test_helper.rb b/test/test_helper.rb
index 0959a8ccd..ca6a4b4f2 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -26,15 +26,18 @@ class ActiveSupport::TestCase
include DownloadTestHelper
include IqdbTestHelper
include UploadTestHelper
+ extend StripeTestHelper
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|
diff --git a/test/test_helpers/stripe_test_helper.rb b/test/test_helpers/stripe_test_helper.rb
new file mode 100644
index 000000000..9bfc845f9
--- /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 unless UserUpgrade.enabled?
+ end
+
+ teardown do
+ StripeMock.stop unless UserUpgrade.enabled?
+ end
+ end
+end
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")
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
diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb
new file mode 100644
index 000000000..d57ef52ce
--- /dev/null
+++ b/test/unit/autocomplete_service_test.rb
@@ -0,0 +1,198 @@
+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([], "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
+ 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
+
+ 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_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
+ 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)
+ 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 "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)
+ 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
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
diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb
index 193cbbad5..b38ce79e8 100644
--- a/test/unit/post_query_builder_test.rb
+++ b/test/unit/post_query_builder_test.rb
@@ -191,9 +191,15 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([post1], "fav:#{user1.name}")
assert_tag_match([post2], "fav:#{user2.name}")
assert_tag_match([], "fav:#{user3.name}")
+
+ assert_tag_match([], "fav:#{user1.name} fav:#{user2.name}")
+ assert_tag_match([post1], "fav:#{user1.name} -fav:#{user2.name}")
+ assert_tag_match([post3], "-fav:#{user1.name} -fav:#{user2.name}")
+
assert_tag_match([], "fav:dne")
assert_tag_match([post3, post2], "-fav:#{user1.name}")
+ assert_tag_match([post3], "-fav:#{user1.name} -fav:#{user2.name}")
assert_tag_match([post3, post2, post1], "-fav:dne")
as(user3) do
@@ -1018,6 +1024,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..c2fa690fa 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
@@ -1060,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
@@ -1190,6 +1213,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/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/session_loader_test.rb b/test/unit/session_loader_test.rb
index 075eca261..6706c6e9c 100644
--- a/test/unit/session_loader_test.rb
+++ b/test/unit/session_loader_test.rb
@@ -6,10 +6,12 @@ class SessionLoaderTest < ActiveSupport::TestCase
@request = mock
@request.stubs(:host).returns("danbooru")
@request.stubs(:remote_ip).returns("127.0.0.1")
+ @request.stubs(:path).returns("/")
@request.stubs(:authorization).returns(nil)
@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
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/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/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)
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
diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb
index ede77bb90..a638dc9a9 100644
--- a/test/unit/tag_test.rb
+++ b/test/unit/tag_test.rb
@@ -13,16 +13,6 @@ class TagTest < ActiveSupport::TestCase
end
context "A tag category fetcher" do
- should "fetch for a single tag" do
- FactoryBot.create(:artist_tag, :name => "test")
- assert_equal(Tag.categories.artist, Tag.category_for("test"))
- end
-
- should "fetch for a single tag with strange markup" do
- FactoryBot.create(:artist_tag, :name => "!@$%")
- assert_equal(Tag.categories.artist, Tag.category_for("!@$%"))
- end
-
should "fetch for multiple tags" do
FactoryBot.create(:artist_tag, :name => "aaa")
FactoryBot.create(:copyright_tag, :name => "bbb")
@@ -169,6 +159,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)
@@ -178,6 +175,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|
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
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
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index 36d34af45..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"))
@@ -45,7 +118,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?)
diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb
new file mode 100644
index 000000000..d73eee270
--- /dev/null
+++ b/test/unit/user_upgrade_test.rb
@@ -0,0 +1,209 @@
+require 'test_helper'
+
+class UserUpgradeTest < ActiveSupport::TestCase
+ context "UserUpgrade:" do
+ context "the #process_upgrade! method" do
+ context "for a self upgrade" do
+ context "to Gold" do
+ setup do
+ @user_upgrade = create(:self_gold_upgrade)
+ end
+
+ 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
+
+ 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
+
+ context "for each upgrade type" do
+ setup do
+ skip unless UserUpgrade.enabled?
+ end
+
+ should "choose the right price in USD for a gold upgrade" do
+ @user_upgrade = create(:self_gold_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "US")
+
+ assert_equal(UserUpgrade.gold_price, @user_upgrade.payment_intent.amount)
+ assert_equal("usd", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right price in USD for a platinum upgrade" do
+ @user_upgrade = create(:self_platinum_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "US")
+
+ assert_equal(UserUpgrade.platinum_price, @user_upgrade.payment_intent.amount)
+ assert_equal("usd", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right price in USD for a gold to platinum upgrade" do
+ @user_upgrade = create(:self_gold_to_platinum_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "US")
+
+ assert_equal(UserUpgrade.gold_to_platinum_price, @user_upgrade.payment_intent.amount)
+ assert_equal("usd", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right price in EUR for a gold upgrade" do
+ @user_upgrade = create(:self_gold_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "DE")
+
+ assert_equal(0.8 * UserUpgrade.gold_price, @user_upgrade.payment_intent.amount)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right price in EUR for a platinum upgrade" do
+ @user_upgrade = create(:self_platinum_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "DE")
+
+ assert_equal(0.8 * UserUpgrade.platinum_price, @user_upgrade.payment_intent.amount)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right price in EUR for a gold to platinum upgrade" do
+ @user_upgrade = create(:self_gold_to_platinum_upgrade)
+ @checkout = @user_upgrade.create_checkout!(country: "DE")
+
+ assert_equal(0.8 * UserUpgrade.gold_to_platinum_price, @user_upgrade.payment_intent.amount)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+ end
+
+ context "for each country" do
+ setup do
+ @user_upgrade = create(:self_gold_upgrade)
+ skip unless UserUpgrade.enabled?
+ end
+
+ should "choose the right payment methods for US" do
+ @checkout = @user_upgrade.create_checkout!(country: "US")
+
+ assert_equal(["card"], @checkout.payment_method_types)
+ assert_equal("usd", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for AT" do
+ @checkout = @user_upgrade.create_checkout!(country: "AT")
+
+ assert_equal(["card", "eps"], @checkout.payment_method_types)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for BE" do
+ @checkout = @user_upgrade.create_checkout!(country: "BE")
+
+ assert_equal(["card", "bancontact"], @checkout.payment_method_types)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for DE" do
+ @checkout = @user_upgrade.create_checkout!(country: "DE")
+
+ assert_equal(["card", "giropay"], @checkout.payment_method_types)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for NL" do
+ @checkout = @user_upgrade.create_checkout!(country: "NL")
+
+ assert_equal(["card", "ideal"], @checkout.payment_method_types)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for PL" do
+ @checkout = @user_upgrade.create_checkout!(country: "PL")
+
+ assert_equal(["card", "p24"], @checkout.payment_method_types)
+ assert_equal("eur", @user_upgrade.payment_intent.currency)
+ end
+
+ should "choose the right payment methods for an unsupported country" do
+ @checkout = @user_upgrade.create_checkout!(country: "MX")
+
+ assert_equal(["card"], @checkout.payment_method_types)
+ assert_equal("usd", @user_upgrade.payment_intent.currency)
+ end
+ end
+ end
+
+ context "the #receipt_url method" do
+ mock_stripe!
+
+ context "a pending upgrade" do
+ should "not have a receipt" do
+ skip unless UserUpgrade.enabled?
+
+ @user_upgrade = create(:self_gold_upgrade, status: "pending")
+ @user_upgrade.create_checkout!
+
+ assert_equal(nil, @user_upgrade.receipt_url)
+ end
+ end
+
+ context "a complete upgrade" do
+ # XXX not supported yet by stripe-ruby-mock
+ should_eventually "have a receipt" do
+ @user_upgrade = create(:self_gold_upgrade, status: "complete")
+ @user_upgrade.create_checkout!
+
+ assert_equal("xxx", @user_upgrade.receipt_url)
+ end
+ end
+ end
+
+ context "the #refund! method" do
+ should_eventually "refund a Gold upgrade" do
+ @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete")
+ @user_upgrade.create_checkout!
+ @user_upgrade.refund!
+
+ assert_equal("refunded", @user_upgrade.reload.status)
+ assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level)
+ end
+ end
+ end
+end
diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb
index bd52fc719..150561e3b 100644
--- a/test/unit/wiki_page_test.rb
+++ b/test/unit/wiki_page_test.rb
@@ -77,5 +77,51 @@ class WikiPageTest < ActiveSupport::TestCase
assert_equal(0, @wiki_page.dtext_links.size)
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)
+ 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)
+ 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
diff --git a/yarn.lock b/yarn.lock
index f52a73199..fbc6f5550 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,10 +2,10 @@
# yarn lockfile v1
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
- integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+ integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
"@babel/highlight" "^7.10.4"
@@ -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", "@babel/generator@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
+ integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
dependencies:
- "@babel/types" "^7.12.5"
+ "@babel/types" "^7.12.11"
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,23 +59,6 @@
"@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==
- dependencies:
- "@babel/helper-annotate-as-pure" "^7.10.4"
- "@babel/helper-module-imports" "^7.12.1"
- "@babel/types" "^7.12.1"
-
-"@babel/helper-builder-react-jsx@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d"
- integrity sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==
- dependencies:
- "@babel/helper-annotate-as-pure" "^7.10.4"
- "@babel/types" "^7.10.4"
-
"@babel/helper-compilation-targets@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831"
@@ -122,21 +104,21 @@
dependencies:
"@babel/types" "^7.12.1"
-"@babel/helper-function-name@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
- integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
+ integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
dependencies:
- "@babel/helper-get-function-arity" "^7.10.4"
- "@babel/template" "^7.10.4"
- "@babel/types" "^7.10.4"
+ "@babel/helper-get-function-arity" "^7.12.10"
+ "@babel/template" "^7.12.7"
+ "@babel/types" "^7.12.11"
-"@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==
+"@babel/helper-get-function-arity@^7.12.10":
+ 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"
@@ -145,7 +127,7 @@
dependencies:
"@babel/types" "^7.10.4"
-"@babel/helper-member-expression-to-functions@^7.12.1":
+"@babel/helper-member-expression-to-functions@^7.12.1", "@babel/helper-member-expression-to-functions@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855"
integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==
@@ -174,12 +156,12 @@
"@babel/types" "^7.12.1"
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==
+"@babel/helper-optimise-call-expression@^7.10.4", "@babel/helper-optimise-call-expression@^7.12.10":
+ 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"
@@ -196,14 +178,14 @@
"@babel/types" "^7.12.1"
"@babel/helper-replace-supers@^7.12.1":
- version "7.12.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9"
- integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d"
+ integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==
dependencies:
- "@babel/helper-member-expression-to-functions" "^7.12.1"
- "@babel/helper-optimise-call-expression" "^7.10.4"
- "@babel/traverse" "^7.12.5"
- "@babel/types" "^7.12.5"
+ "@babel/helper-member-expression-to-functions" "^7.12.7"
+ "@babel/helper-optimise-call-expression" "^7.12.10"
+ "@babel/traverse" "^7.12.10"
+ "@babel/types" "^7.12.11"
"@babel/helper-simple-access@^7.12.1":
version "7.12.1"
@@ -219,22 +201,22 @@
dependencies:
"@babel/types" "^7.12.1"
-"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
- version "7.11.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
- integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
+"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
+ integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
dependencies:
- "@babel/types" "^7.11.0"
+ "@babel/types" "^7.12.11"
-"@babel/helper-validator-identifier@^7.10.4":
- version "7.10.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
- integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+ integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
-"@babel/helper-validator-option@^7.12.1":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9"
- integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==
+"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f"
+ integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==
"@babel/helper-wrap-function@^7.10.4":
version "7.12.3"
@@ -264,15 +246,15 @@
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.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
+ integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
"@babel/plugin-proposal-async-generator-functions@^7.12.1":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e"
- integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==
+ version "7.12.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
+ integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/helper-remap-async-to-generator" "^7.12.1"
@@ -287,9 +269,9 @@
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-proposal-decorators@^7.10.5":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.1.tgz#59271439fed4145456c41067450543aee332d15f"
- integrity sha512-knNIuusychgYN8fGJHONL0RbFxLGawhXOJNLBk75TniTsZZeA+wdkDuv6wp4lGwzQEKjZi6/WYtnb3udNPmQmQ==
+ version "7.12.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.12.tgz#067a6d3d6ca86d54cf56bb183239199c20daeafe"
+ integrity sha512-fhkE9lJYpw2mjHelBpM2zCbaA11aov2GJs7q4cFaXNrWx0H3bW58H9Esy2rdtYOghFBEYUDRIpvlgi+ZD+AvvQ==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.12.1"
"@babel/helper-plugin-utils" "^7.10.4"
@@ -506,10 +488,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
-"@babel/plugin-transform-block-scoping@^7.12.1":
- version "7.12.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1"
- integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==
+"@babel/plugin-transform-block-scoping@^7.12.11":
+ version "7.12.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca"
+ integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
@@ -668,14 +650,15 @@
"@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.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e"
+ integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==
dependencies:
- "@babel/helper-builder-react-jsx" "^7.10.4"
- "@babel/helper-builder-react-jsx-experimental" "^7.12.4"
+ "@babel/helper-annotate-as-pure" "^7.12.10"
+ "@babel/helper-module-imports" "^7.12.5"
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-jsx" "^7.12.1"
+ "@babel/types" "^7.12.12"
"@babel/plugin-transform-regenerator@^7.10.1", "@babel/plugin-transform-regenerator@^7.12.1":
version "7.12.1"
@@ -692,13 +675,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 +712,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,15 +735,15 @@
"@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.11"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9"
+ integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==
dependencies:
"@babel/compat-data" "^7.12.7"
"@babel/helper-compilation-targets" "^7.12.5"
"@babel/helper-module-imports" "^7.12.5"
"@babel/helper-plugin-utils" "^7.10.4"
- "@babel/helper-validator-option" "^7.12.1"
+ "@babel/helper-validator-option" "^7.12.11"
"@babel/plugin-proposal-async-generator-functions" "^7.12.1"
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-dynamic-import" "^7.12.1"
@@ -790,7 +772,7 @@
"@babel/plugin-transform-arrow-functions" "^7.12.1"
"@babel/plugin-transform-async-to-generator" "^7.12.1"
"@babel/plugin-transform-block-scoped-functions" "^7.12.1"
- "@babel/plugin-transform-block-scoping" "^7.12.1"
+ "@babel/plugin-transform-block-scoping" "^7.12.11"
"@babel/plugin-transform-classes" "^7.12.1"
"@babel/plugin-transform-computed-properties" "^7.12.1"
"@babel/plugin-transform-destructuring" "^7.12.1"
@@ -816,12 +798,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.11"
+ core-js-compat "^3.8.0"
semver "^5.5.0"
"@babel/preset-modules@^0.1.3":
@@ -859,27 +841,27 @@
"@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.12"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
+ integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
dependencies:
- "@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.12.5"
- "@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/code-frame" "^7.12.11"
+ "@babel/generator" "^7.12.11"
+ "@babel/helper-function-name" "^7.12.11"
+ "@babel/helper-split-export-declaration" "^7.12.11"
+ "@babel/parser" "^7.12.11"
+ "@babel/types" "^7.12.12"
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.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
+ version "7.12.12"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
+ integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
dependencies:
- "@babel/helper-validator-identifier" "^7.10.4"
+ "@babel/helper-validator-identifier" "^7.12.11"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
@@ -888,10 +870,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"
@@ -909,25 +891,25 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz#ccfef6ddbe59f8fe8f694783e1d3eb88902dc5eb"
integrity sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ==
-"@nodelib/fs.scandir@2.1.3":
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
- integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+"@nodelib/fs.scandir@2.1.4":
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69"
+ integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==
dependencies:
- "@nodelib/fs.stat" "2.0.3"
+ "@nodelib/fs.stat" "2.0.4"
run-parallel "^1.1.9"
-"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
- integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655"
+ integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==
"@nodelib/fs.walk@^1.2.3":
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
- integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063"
+ integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==
dependencies:
- "@nodelib/fs.scandir" "2.1.3"
+ "@nodelib/fs.scandir" "2.1.4"
fastq "^1.6.0"
"@npmcli/move-file@^1.0.1":
@@ -938,14 +920,14 @@
mkdirp "^1.0.4"
"@popperjs/core@^2.4.4":
- version "2.5.4"
- resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.5.4.tgz#de25b5da9f727985a3757fd59b5d028aba75841a"
- integrity sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
+ integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
"@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 +1053,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.19"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.19.tgz#5135176a8330b88ece4e9ab1fdcfc0a545b4bab4"
+ integrity sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -1270,7 +1252,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==
@@ -1313,6 +1295,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ajv@^7.0.2:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2"
+ integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
+
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -1510,11 +1502,6 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
-astral-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
- integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
-
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -1835,14 +1822,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.16.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 +1963,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 +2049,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.30001171"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001171.tgz#3291e11e02699ad0a29e69b8d407666fc843eba7"
+ integrity sha512-5Alrh8TTYPG9IH4UkRqEBZoEToWRLvPbSQokvzSz0lii8/FOWKG4keO1HoYfPWs8IF/NH/dyNPg1cmJGvV3Zlg==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.3.0"
@@ -2444,23 +2431,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.2"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.2.tgz#3717f51f6c3d2ebba8cbf27619b57160029d1d4c"
+ integrity sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==
dependencies:
- browserslist "^4.14.7"
+ browserslist "^4.16.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.2"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.2.tgz#286f885c0dac1cdcd6d78397392abc25ddeca225"
+ integrity sha512-v6zfIQqL/pzTVAbZvYUozsxNfxcFb6Ks3ZfEbuneJl3FW9Jb8F6vLWB6f+qTmAu72msUdyb84V8d/yBFf7FNnw==
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.2"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.2.tgz#0a1fd6709246da9ca8eff5bb0cbd15fba9ac7044"
+ integrity sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -2781,7 +2768,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
dependencies:
ms "2.0.0"
-debug@^3.1.1, debug@^3.2.5:
+debug@^3.1.1, debug@^3.2.6:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -3111,10 +3098,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.633"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe"
+ integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==
elliptic@^6.5.3:
version "6.5.3"
@@ -3183,9 +3170,9 @@ entities@^2.0.0:
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
errno@^0.1.3, errno@~0.1.7:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
- integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+ integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
dependencies:
prr "~1.0.1"
@@ -3324,12 +3311,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.17.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0"
+ integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==
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 +3326,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"
@@ -3362,17 +3349,17 @@ eslint@^7.0.0:
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
- table "^5.2.3"
+ table "^6.0.4"
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:
@@ -3607,20 +3594,13 @@ fastest-levenshtein@^1.0.12:
integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
fastq@^1.6.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947"
- integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb"
+ integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==
dependencies:
reusify "^1.0.4"
-faye-websocket@^0.10.0:
- version "0.10.0"
- resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
- integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
- dependencies:
- websocket-driver ">=0.5.1"
-
-faye-websocket@~0.11.1:
+faye-websocket@^0.11.3:
version "0.11.3"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==
@@ -3639,13 +3619,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 +3735,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 +3743,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 +3762,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"
@@ -3959,9 +3918,9 @@ get-installed-path@4.0.8:
global-modules "1.0.0"
get-intrinsic@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be"
- integrity sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
+ integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
@@ -4329,9 +4288,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.4.0"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc"
+ integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==
html-tags@^3.1.0:
version "3.1.0"
@@ -4393,9 +4352,9 @@ http-errors@~1.7.2:
toidentifier "1.0.0"
http-parser-js@>=0.5.1:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77"
- integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9"
+ integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==
http-proxy-middleware@0.19.1:
version "0.19.1"
@@ -4493,9 +4452,9 @@ import-fresh@^2.0.0:
resolve-from "^3.0.0"
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.2.tgz#fc129c160c5d68235507f4331a6baad186bdbc3e"
- integrity sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+ integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
@@ -4576,9 +4535,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 +4605,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 +4780,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 +4959,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 +4967,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"
@@ -5041,6 +5010,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -5056,7 +5030,7 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
-json3@^3.3.2:
+json3@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
@@ -5364,19 +5338,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.2"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.2.tgz#8fe6f42a2683c43c5609dfb40407c095409c85b4"
+ integrity sha512-iRczns6WMvu0hUw02LXsPDJshBIwtUPbvHBWo19IQeU0YqmzlA8Pd30U8V7uiI0VPkxzS7A/NXBXH6u+HS87Zg==
dependencies:
"@types/unist" "^2.0.0"
longest-streak "^2.0.0"
@@ -5385,11 +5360,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"
@@ -5443,9 +5413,9 @@ meow@^3.7.0:
trim-newlines "^1.0.0"
meow@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-8.0.0.tgz#1aa10ee61046719e334ffdc038bb5069250ec99a"
- integrity sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.0.tgz#0fcaa267e35e4d58584b8205923df6021ddcc7ba"
+ integrity sha512-fNWkgM1UVMey2kf24yLiccxLihc5W+6zVus3/N0b+VfnJgxV99E9u04X6NAiKdg6ED7DAQBX5sy36NM0QJZkWA==
dependencies:
"@types/minimist" "^1.2.0"
camelcase-keys "^6.2.2"
@@ -5479,10 +5449,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"
@@ -5522,22 +5492,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.44.0:
- version "1.44.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
- integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
-
-"mime-db@>= 1.43.0 < 2":
+mime-db@1.45.0, "mime-db@>= 1.43.0 < 2":
version "1.45.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
- version "2.1.27"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
- integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+ version "2.1.28"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
+ integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
dependencies:
- mime-db "1.44.0"
+ mime-db "1.45.0"
mime@1.6.0:
version "1.6.0"
@@ -5545,9 +5510,9 @@ mime@1.6.0:
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.4.4:
- version "2.4.6"
- resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
- integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
+ version "2.4.7"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
+ integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
mimic-fn@^2.1.0:
version "2.1.0"
@@ -5721,11 +5686,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"
@@ -6038,9 +6008,9 @@ object-copy@^0.1.0:
kind-of "^3.0.3"
object-hash@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
- integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09"
+ integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==
object-inspect@^1.8.0:
version "1.9.0"
@@ -7103,9 +7073,9 @@ postcss-selector-matches@^4.0.0:
postcss "^7.0.2"
postcss-selector-not@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0"
- integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf"
+ integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==
dependencies:
balanced-match "^1.0.0"
postcss "^7.0.2"
@@ -7191,9 +7161,9 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2
supports-color "^6.1.0"
preact@^10.4.6:
- version "10.5.7"
- resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.7.tgz#f1d84725539e18f7ccbea937cf3db5895661dbd3"
- integrity sha512-4oEpz75t/0UNcwmcsjk+BIcDdk68oao+7kxcpc1hQPNs2Oo3ZL9xFz8UBf350mxk/VEdD41L5b4l2dE3Ug3RYg==
+ version "10.5.9"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.9.tgz#8caba9288b4db1d593be2317467f8735e43cda0b"
+ integrity sha512-X4m+4VMVINl/JFQKALOCwa3p8vhMAhBvle0hJ/W44w/WWfNb2TA7RNicDV3K2dNVs57f61GviEnVLiwN+fxiIg==
prelude-ls@^1.2.1:
version "1.2.1"
@@ -7572,11 +7542,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 +7579,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"
@@ -7645,6 +7610,11 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
@@ -7700,7 +7670,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 +7717,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"
@@ -7876,7 +7839,7 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
-selfsigned@^1.10.7:
+selfsigned@^1.10.8:
version "1.10.8"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==
@@ -8070,15 +8033,6 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-slice-ansi@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
- integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
- dependencies:
- ansi-styles "^3.2.0"
- astral-regex "^1.0.0"
- is-fullwidth-code-point "^2.0.0"
-
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@@ -8118,26 +8072,26 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
-sockjs-client@1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
- integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==
+sockjs-client@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.0.tgz#2f8ff5d4b659e0d092f7aba0b7c386bd2aa20add"
+ integrity sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q==
dependencies:
- debug "^3.2.5"
+ debug "^3.2.6"
eventsource "^1.0.7"
- faye-websocket "~0.11.1"
- inherits "^2.0.3"
- json3 "^3.3.2"
- url-parse "^1.4.3"
+ faye-websocket "^0.11.3"
+ inherits "^2.0.4"
+ json3 "^3.3.3"
+ url-parse "^1.4.7"
-sockjs@0.3.20:
- version "0.3.20"
- resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855"
- integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==
+sockjs@^0.3.21:
+ version "0.3.21"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417"
+ integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==
dependencies:
- faye-websocket "^0.10.0"
+ faye-websocket "^0.11.3"
uuid "^3.4.0"
- websocket-driver "0.6.5"
+ websocket-driver "^0.7.4"
sort-keys@^1.0.0:
version "1.1.2"
@@ -8645,22 +8599,12 @@ svgo@^1.0.0:
unquote "~1.1.1"
util.promisify "~1.0.0"
-table@^5.2.3:
- version "5.4.6"
- resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
- integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+table@^6.0.3, table@^6.0.4:
+ version "6.0.6"
+ resolved "https://registry.yarnpkg.com/table/-/table-6.0.6.tgz#e9223f1e851213e2e43ab584b0fec33fb09a8e7a"
+ integrity sha512-OInCtPmDNieVBkVFi6C8RwU2S2H0h8mF3e3TQK4nreaUNCpooQUkI+A/KuEkm5FawfhWIfNqG+qfelVVR+V00g==
dependencies:
- ajv "^6.10.2"
- lodash "^4.17.14"
- slice-ansi "^2.1.0"
- string-width "^3.0.0"
-
-table@^6.0.3:
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/table/-/table-6.0.4.tgz#c523dd182177e926c723eb20e1b341238188aa0d"
- integrity sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==
- dependencies:
- ajv "^6.12.4"
+ ajv "^7.0.2"
lodash "^4.17.20"
slice-ansi "^4.0.0"
string-width "^4.2.0"
@@ -9148,7 +9092,7 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
-url-parse@^1.4.3:
+url-parse@^1.4.3, url-parse@^1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
@@ -9249,13 +9193,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"
@@ -9320,9 +9263,9 @@ webpack-cli@^3.3.0, webpack-cli@^3.3.12:
yargs "^13.3.2"
webpack-dev-middleware@^3.7.2:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
- integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
+ version "3.7.3"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
+ integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==
dependencies:
memory-fs "^0.4.1"
mime "^2.4.4"
@@ -9331,9 +9274,9 @@ webpack-dev-middleware@^3.7.2:
webpack-log "^2.0.0"
webpack-dev-server@^3.8.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c"
- integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz#c74028bf5ba8885aaf230e48a20e8936ab8511f0"
+ integrity sha512-u4R3mRzZkbxQVa+MBWi2uVpB5W59H3ekZAJsQlKUTdl7Elcah2EhygTPLmeFXybQkf9i2+L0kn7ik9SnXa6ihQ==
dependencies:
ansi-html "0.0.7"
bonjour "^3.5.0"
@@ -9355,11 +9298,11 @@ webpack-dev-server@^3.8.0:
p-retry "^3.0.1"
portfinder "^1.0.26"
schema-utils "^1.0.0"
- selfsigned "^1.10.7"
+ selfsigned "^1.10.8"
semver "^6.3.0"
serve-index "^1.9.1"
- sockjs "0.3.20"
- sockjs-client "1.4.0"
+ sockjs "^0.3.21"
+ sockjs-client "^1.5.0"
spdy "^4.0.2"
strip-ansi "^3.0.1"
supports-color "^6.1.0"
@@ -9414,14 +9357,7 @@ webpack@^4.44.1:
watchpack "^1.7.4"
webpack-sources "^1.4.1"
-websocket-driver@0.6.5:
- version "0.6.5"
- resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
- integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=
- dependencies:
- websocket-extensions ">=0.1.1"
-
-websocket-driver@>=0.5.1:
+websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
@@ -9506,13 +9442,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 +9450,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"