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 = %{ -
- - #{hidden_field_tag(:desc, desc)} - #{hidden_field_tag(:user_id, user.id)} - -
- } - - 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 @@ -
    - -
    - -<%= 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;" %> - + <% 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 @@

    Contact

    -

    Questions & Comments

    - -

    You can reach the administrator of this site at <%= mail_to Danbooru.config.contact_email, nil, :encode => :hex %>.

    +

    + You can contact the administrator of this site by + <%= link_to "sending a private message", new_dmail_path(dmail: { to_name: User.owner.name }) %> to <%= link_to_user User.owner, "@#{User.owner.name}" %>, + by messaging @<%= User.owner.name %> on the <%= link_to "#{Danbooru.config.canonical_app_name} Discord", Danbooru.config.discord_server_url %>, + or by sending an email to <%= mail_to Danbooru.config.contact_email, nil, encode: :hex %>. +

    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| %> + + <% end %> + + + + <% rows.each do |row| %> + + <% row.each do |key, value| %> + + <% end %> + + <% end %> +
      <%= key.humanize %>
      <%= value %>
      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

      -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      BasicGoldPlatinum
      CostFree - <%= cents_to_usd(UserUpgrade.gold_price) %> -
      One time fee
      -
      - <%= cents_to_usd(UserUpgrade.platinum_price) %> -
      One time fee
      -
      Tag Limit2<%= Danbooru.config.base_tag_query_limit %><%= Danbooru.config.base_tag_query_limit*2 %>
      Favorite Limit10,00020,000Unlimited
      Favorite Groups3510
      Page Limit1,0002,0005,000
      Saved Searches2502501,000
      See Hidden TagsNoYesYes
      Search Timeout3 sec6 sec9 sec
      -
      + <% if @user_upgrade.recipient.is_platinum? %> +

      <%= link_to_user @recipient %> is already above Platinum level and can't be upgraded!

      + <% else %> +
      You are gifting this upgrade to <%= link_to_user @user_upgrade.recipient %>.
      + <% end %> + <% else %> +

      Upgrade Account

      + +

      Upgrading your account gives you exclusive benefits and helps support + <%= Danbooru.config.canonical_app_name %>. Your support helps keep the + site ad-free for everyone!

      + +

      You can also gift an account upgrade to someone else. Just go to + their profile page and look for a "Gift Upgrade" link.

      <% end %> -
      - <% if params[:user_id] %> -

      You are gifting this account upgrade to <%= link_to user.pretty_name, user_path(params[:user_id]) %>.

      - <% else %> -

      You can also upgrade someone else's account for the same price. The easiest way is to go to their profile page and look for a "Gift Upgrade" link.

      - <% end %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <% if @user_upgrade.purchaser.is_anonymous? %> + + + + <% elsif @recipient.level == User::Levels::MEMBER %> + + + + <% elsif @recipient.level == User::Levels::GOLD %> + + + + <% else %> + + + + <% end %> + + +
      BasicGoldPlatinum
      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 TagsNoYesYes
      Page Limit1,0002,0005,000
      Favorite Limit10,00020,000Unlimited
      Favorite Groups3510
      Saved Searches2502501,000
      Search Timeout3 sec6 sec9 sec
      <%= 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" %><%= 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..." %><%= 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..." %><%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %><%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", nil, disabled: true %>
      + +

      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 @@ -
      - -
      - -<%= 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 @@
      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 - - - - - -
      -

      That page does not exist

      -

      Return to index

      -
      - - 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 - - - - - -
      -

      Something broke

      -

      Return to index

      -
      - - 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"