Merge branch 'master' into minor_fix

This commit is contained in:
evazion
2021-01-04 01:30:28 -06:00
committed by GitHub
243 changed files with 4897 additions and 2446 deletions

View File

@@ -40,7 +40,8 @@ gem 'puma'
gem 'scenic' gem 'scenic'
gem 'ipaddress_2' gem 'ipaddress_2'
gem 'http' 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 'pundit'
gem 'mail' gem 'mail'
gem 'nokogiri' gem 'nokogiri'
@@ -59,7 +60,7 @@ end
group :development do group :development do
gem 'rubocop' gem 'rubocop'
gem 'rubocop-rails' gem 'rubocop-rails'
gem 'meta_request' # gem 'meta_request' # hangs on Rails 6.1
gem 'rack-mini-profiler' gem 'rack-mini-profiler'
gem 'stackprof' gem 'stackprof'
gem 'flamegraph' gem 'flamegraph'
@@ -85,4 +86,5 @@ group :test do
gem "capybara" gem "capybara"
gem "selenium-webdriver" gem "selenium-webdriver"
gem "codecov", require: false gem "codecov", require: false
gem 'stripe-ruby-mock', require: "stripe_mock"
end end

View File

@@ -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 GIT
remote: https://github.com/evazion/dtext_rb.git remote: https://github.com/evazion/dtext_rb.git
revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7 revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7
@@ -5,71 +12,81 @@ GIT
dtext_rb (1.10.6) dtext_rb (1.10.6)
nokogiri (~> 1.8) 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 GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.0.3.4) actioncable (6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
activesupport (= 6.1.0)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.4) actionmailbox (6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
activejob (= 6.0.3.4) activejob (= 6.1.0)
activerecord (= 6.0.3.4) activerecord (= 6.1.0)
activestorage (= 6.0.3.4) activestorage (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.0.3.4) actionmailer (6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
actionview (= 6.0.3.4) actionview (= 6.1.0)
activejob (= 6.0.3.4) activejob (= 6.1.0)
activesupport (= 6.1.0)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.0.3.4) actionpack (6.1.0)
actionview (= 6.0.3.4) actionview (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
rack (~> 2.0, >= 2.0.8) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.4) actiontext (6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
activerecord (= 6.0.3.4) activerecord (= 6.1.0)
activestorage (= 6.0.3.4) activestorage (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.0.3.4) actionview (6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.4) activejob (6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.0.3.4) activemodel (6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
activemodel-serializers-xml (1.0.2) activemodel-serializers-xml (1.0.2)
activemodel (> 5.x) activemodel (> 5.x)
activesupport (> 5.x) activesupport (> 5.x)
builder (~> 3.1) builder (~> 3.1)
activerecord (6.0.3.4) activerecord (6.1.0)
activemodel (= 6.0.3.4) activemodel (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
activerecord-hierarchical_query (1.2.3) activestorage (6.1.0)
activerecord (>= 5.0, < 6.1) actionpack (= 6.1.0)
pg (>= 0.21, < 1.3) activejob (= 6.1.0)
activestorage (6.0.3.4) activerecord (= 6.1.0)
actionpack (= 6.0.3.4) activesupport (= 6.1.0)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (6.0.3.4) mimemagic (~> 0.3.2)
activesupport (6.1.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 1.6, < 2)
minitest (~> 5.1) minitest (>= 5.1)
tzinfo (~> 1.1) tzinfo (~> 2.0)
zeitwerk (~> 2.2, >= 2.2.2) zeitwerk (~> 2.3)
addressable (2.7.0) addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.4.0) airbrussh (1.4.0)
@@ -77,13 +94,13 @@ GEM
ansi (1.5.0) ansi (1.5.0)
ast (2.4.1) ast (2.4.1)
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.402.0) aws-partitions (1.414.0)
aws-sdk-core (3.109.3) aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-sqs (1.34.0) aws-sdk-sqs (1.35.0)
aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2) aws-sigv4 (1.2.2)
@@ -120,20 +137,20 @@ GEM
xpath (~> 3.2) xpath (~> 3.2)
childprocess (3.0.0) childprocess (3.0.0)
chronic (0.10.2) chronic (0.10.2)
codecov (0.2.12) codecov (0.2.15)
json simplecov (>= 0.15, < 0.21)
simplecov
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.1.7) concurrent-ruby (1.1.7)
crass (1.0.6) crass (1.0.6)
daemons (1.3.1) daemons (1.3.1)
delayed_job (4.1.8) dante (0.2.0)
activesupport (>= 3.0, < 6.1) delayed_job (4.1.9)
delayed_job_active_record (4.1.4) activesupport (>= 3.0, < 6.2)
activerecord (>= 3.0, < 6.1) delayed_job_active_record (4.1.5)
activerecord (>= 3.0, < 6.2)
delayed_job (>= 3.0, < 5) delayed_job (>= 3.0, < 5)
diff-lcs (1.4.4) diff-lcs (1.4.4)
docile (1.3.2) docile (1.3.4)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6) dotenv (2.7.6)
@@ -143,11 +160,13 @@ GEM
erubi (1.10.0) erubi (1.10.0)
factory_bot (6.1.0) factory_bot (6.1.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
faraday (1.1.0) faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ruby2_keywords ruby2_keywords
faraday-net_http (1.0.0)
ffaker (2.17.0) ffaker (2.17.0)
ffi (1.13.1) ffi (1.14.2)
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
@@ -161,19 +180,17 @@ GEM
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
http-parser (~> 1.2.0) http-parser (~> 1.2.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.3.0) http-form_data (2.3.0)
http-parser (1.2.2) http-parser (1.2.2)
ffi-compiler ffi-compiler
i18n (1.8.5) i18n (1.8.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ipaddress_2 (0.13.0) ipaddress_2 (0.13.0)
jmespath (1.4.0) jmespath (1.4.0)
json (2.3.1) json (2.5.1)
jwt (2.2.2) jwt (2.2.2)
kgio (2.11.3) kgio (2.11.3)
listen (3.3.3) listen (3.4.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.8.0) loofah (2.8.0)
@@ -185,9 +202,6 @@ GEM
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
memoist (0.16.2) memoist (0.16.2)
memory_profiler (1.0.0) 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) method_source (1.0.0)
mimemagic (0.3.5) mimemagic (0.3.5)
mini_mime (1.0.2) mini_mime (1.0.2)
@@ -200,7 +214,7 @@ GEM
builder builder
minitest (>= 5.0) minitest (>= 5.0)
ruby-progressbar ruby-progressbar
mocha (1.11.2) mocha (1.12.0)
mock_redis (0.26.0) mock_redis (0.26.0)
msgpack (1.3.3) msgpack (1.3.3)
multi_json (1.15.0) multi_json (1.15.0)
@@ -224,7 +238,7 @@ GEM
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
parallel (1.20.1) parallel (1.20.1)
parser (2.7.2.0) parser (3.0.0.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pg (1.2.3) pg (1.2.3)
pry (0.13.1) pry (0.13.1)
@@ -236,48 +250,46 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.1.0) puma (5.1.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (2.2.3) rack (2.2.3)
rack-contrib (2.3.0) rack-mini-profiler (2.3.0)
rack (~> 2.0)
rack-mini-profiler (2.2.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.0.3.4) rails (6.1.0)
actioncable (= 6.0.3.4) actioncable (= 6.1.0)
actionmailbox (= 6.0.3.4) actionmailbox (= 6.1.0)
actionmailer (= 6.0.3.4) actionmailer (= 6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
actiontext (= 6.0.3.4) actiontext (= 6.1.0)
actionview (= 6.0.3.4) actionview (= 6.1.0)
activejob (= 6.0.3.4) activejob (= 6.1.0)
activemodel (= 6.0.3.4) activemodel (= 6.1.0)
activerecord (= 6.0.3.4) activerecord (= 6.1.0)
activestorage (= 6.0.3.4) activestorage (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
bundler (>= 1.3.0) bundler (>= 1.15.0)
railties (= 6.0.3.4) railties (= 6.1.0)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0) rails-html-sanitizer (1.3.0)
loofah (~> 2.3) loofah (~> 2.3)
railties (6.0.3.4) railties (6.1.0)
actionpack (= 6.0.3.4) actionpack (= 6.1.0)
activesupport (= 6.0.3.4) activesupport (= 6.1.0)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.19.1) raindrops (0.19.1)
rake (13.0.1) rake (13.0.3)
rakismet (1.5.4) rakismet (1.5.4)
rb-fsevent (0.10.4) rb-fsevent (0.10.4)
rb-inotify (0.10.1) rb-inotify (0.10.1)
@@ -292,22 +304,22 @@ GEM
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
rexml (3.2.4) rexml (3.2.4)
rubocop (1.4.2) rubocop (1.7.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.7.1.5) parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8) regexp_parser (>= 1.8, < 3.0)
rexml rexml
rubocop-ast (>= 1.1.1) rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0) unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (1.3.0) rubocop-ast (1.4.0)
parser (>= 2.7.1.5) parser (>= 2.7.1.5)
rubocop-rails (2.8.1) rubocop-rails (2.9.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.87.0) rubocop (>= 0.90.0, < 2.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.11.0)
ruby-vips (2.0.17) ruby-vips (2.0.17)
ffi (~> 1.9) ffi (~> 1.9)
ruby2_keywords (0.0.2) ruby2_keywords (0.0.2)
@@ -349,15 +361,18 @@ GEM
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
stripe (5.28.0) 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) thor (1.0.1)
thread_safe (0.3.6) tzinfo (2.0.4)
tzinfo (1.2.8) concurrent-ruby (~> 1.0)
thread_safe (~> 0.1)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.7) unf_ext (0.0.7.7)
unicode-display_width (1.7.0) unicode-display_width (1.7.0)
unicorn (5.7.0) unicorn (5.8.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
unicorn-worker-killer (0.4.4) unicorn-worker-killer (0.4.4)
@@ -382,7 +397,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
activemodel-serializers-xml activemodel-serializers-xml
activerecord-hierarchical_query activerecord-hierarchical_query!
addressable addressable
aws-sdk-sqs (~> 1) aws-sdk-sqs (~> 1)
bcrypt bcrypt
@@ -405,12 +420,12 @@ DEPENDENCIES
ffaker ffaker
flamegraph flamegraph
http http
http-cookie!
ipaddress_2 ipaddress_2
listen listen
mail mail
memoist memoist
memory_profiler memory_profiler
meta_request
minitest-ci minitest-ci
minitest-reporters minitest-reporters
mocha mocha
@@ -446,6 +461,7 @@ DEPENDENCIES
stackprof stackprof
streamio-ffmpeg streamio-ffmpeg
stripe stripe
stripe-ruby-mock
unicorn unicorn
unicorn-worker-killer unicorn-worker-killer
webpacker (>= 4.0.x) webpacker (>= 4.0.x)

View File

@@ -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 $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 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 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 - 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 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 usermod -G danbooru,sudo danbooru
# Set up Postgres # 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 if verlte 9.5 $PG_VERSION ; then
# only do this on postgres 9.5 and above # only do this on postgres 9.5 and above

View File

@@ -6,7 +6,13 @@ module Admin
def update def update
@user = authorize User.find(params[:id]), :promote? @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" redirect_to edit_admin_user_path(@user), :notice => "User updated"
end end
end end

View File

@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
before_action :set_variant before_action :set_variant
before_action :add_headers before_action :add_headers
before_action :cause_error before_action :cause_error
after_action :skip_session_if_publicly_cached
after_action :reset_current_user after_action :reset_current_user
layout "default" layout "default"
@@ -87,6 +88,8 @@ class ApplicationController < ActionController::Base
end end
def rescue_exception(exception) def rescue_exception(exception)
raise exception if Danbooru.config.debug_mode
case exception case exception
when ActionView::Template::Error when ActionView::Template::Error
rescue_exception(exception.cause) rescue_exception(exception.cause)
@@ -121,17 +124,17 @@ class ApplicationController < ActionController::Base
end end
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 @exception = exception
@expected = status < 500 @expected = status < 500
@message = message.encode("utf-8", invalid: :replace, undef: :replace) @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]) 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. # if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
layout = CurrentUser.user.present? ? "default" : "blank" 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 render template, layout: layout, status: status, formats: format
rescue ActionView::MissingTemplate rescue ActionView::MissingTemplate
render "static/error", layout: layout, status: status, formats: format render "static/error", layout: layout, status: status, formats: format
@@ -148,6 +151,14 @@ class ApplicationController < ActionController::Base
CurrentUser.root_url = root_url.chomp("/") CurrentUser.root_url = root_url.chomp("/")
end 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 def set_variant
request.variant = params[:variant].try(:to_sym) request.variant = params[:variant].try(:to_sym)
end end

View File

@@ -2,15 +2,16 @@ class AutocompleteController < ApplicationController
respond_to :xml, :json respond_to :xml, :json
def index 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? @results = @autocomplete.autocomplete_results
expires_in 1.hour @expires_in = @autocomplete.cache_duration
results = [params[:query], @tags.map(&:pretty_name)] @public = @autocomplete.cache_publicly?
respond_with(results)
else expires_in @expires_in, public: @public unless response.cache_control.present?
# XXX respond_with(@results)
respond_with(@tags.map(&:attributes))
end
end end
end end

View File

@@ -1,8 +1,19 @@
class EmailsController < ApplicationController class EmailsController < ApplicationController
respond_to :html, :xml, :json 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 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) respond_with(@email_address)
end end
@@ -17,7 +28,7 @@ class EmailsController < ApplicationController
if @user.authenticate_password(params[:user][:password]) if @user.authenticate_password(params[:user][:password])
@user.update(email_address_attributes: { address: params[:user][:email] }) @user.update(email_address_attributes: { address: params[:user][:email] })
else else
@user.errors[:base] << "Password was incorrect" @user.errors.add(:base, "Password was incorrect")
end end
if @user.errors.none? if @user.errors.none?

View File

@@ -17,18 +17,10 @@ class LegacyController < ApplicationController
end end
end end
def users
@users = User.limit(100).search(params).paginate(params[:page])
end
def tags def tags
@tags = Tag.limit(100).search(params).paginate(params[:page], :limit => params[:limit]) @tags = Tag.limit(100).search(params).paginate(params[:page], :limit => params[:limit])
end end
def artists
@artists = Artist.limit(100).search(search_params).paginate(params[:page])
end
def unavailable def unavailable
render :plain => "this resource is no longer available", :status => 410 render :plain => "this resource is no longer available", :status => 410
end end

View File

@@ -1,9 +1,6 @@
class NotesController < ApplicationController class NotesController < ApplicationController
respond_to :html, :xml, :json, :js respond_to :html, :xml, :json, :js
def search
end
def index def index
@notes = authorize Note.paginated_search(params) @notes = authorize Note.paginated_search(params)
@notes = @notes.includes(:post) if request.format.html? @notes = @notes.includes(:post) if request.format.html?

View File

@@ -9,10 +9,10 @@ class PasswordsController < ApplicationController
def update def update
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy @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]) @user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
else else
@user.errors[:base] << "Incorrect password" @user.errors.add(:base, "Incorrect password")
end end
flash[:notice] = @user.errors.none? ? "Password updated" : @user.errors.full_messages.join("; ") flash[:notice] = @user.errors.none? ? "Password updated" : @user.errors.full_messages.join("; ")

View File

@@ -6,12 +6,6 @@ class SavedSearchesController < ApplicationController
respond_with(@saved_searches) respond_with(@saved_searches)
end 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 def create
@saved_search = authorize SavedSearch.new(user: CurrentUser.user, **permitted_attributes(SavedSearch)) @saved_search = authorize SavedSearch.new(user: CurrentUser.user, **permitted_attributes(SavedSearch))
@saved_search.save @saved_search.save

View File

@@ -1,4 +1,6 @@
class StaticController < ApplicationController class StaticController < ApplicationController
respond_to :html, :json, :xml
def privacy_policy def privacy_policy
end end
@@ -6,7 +8,11 @@ class StaticController < ApplicationController
end end
def not_found 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 end
def error def error
@@ -38,7 +44,7 @@ class StaticController < ApplicationController
@search = { is_deleted: "false" } @search = { is_deleted: "false" }
when "posts" when "posts"
@relation = Post.order(id: :asc) @relation = Post.order(id: :asc)
@serach = {} @search = {}
when "tags" when "tags"
@relation = Tag.nonempty @relation = Tag.nonempty
@search = {} @search = {}

View File

@@ -0,0 +1,8 @@
class StatusController < ApplicationController
respond_to :html, :json, :xml
def show
@status = ServerStatus.new
respond_with(@status)
end
end

View File

@@ -12,18 +12,6 @@ class TagsController < ApplicationController
respond_with(@tags) respond_with(@tags)
end 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 def show
@tag = authorize Tag.find(params[:id]) @tag = authorize Tag.find(params[:id])
respond_with(@tag) respond_with(@tag)

View File

@@ -1,64 +1,59 @@
class UserUpgradesController < ApplicationController class UserUpgradesController < ApplicationController
helper_method :user respond_to :js, :html, :json, :xml
skip_before_action :verify_authenticity_token, only: [:create]
def create def create
if params[:stripeToken] @user_upgrade = authorize UserUpgrade.create(recipient: recipient, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type])
create_stripe @country = params[:country] || CurrentUser.country || "US"
end @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 end
def new 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 end
def show def show
authorize User, :upgrade? @user_upgrade = authorize UserUpgrade.find(params[:id])
respond_with(@user_upgrade)
end 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] if params[:user_id]
User.find(params[:user_id]) User.find(params[:user_id])
else else
CurrentUser.user CurrentUser.user
end end
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 end

View File

@@ -38,9 +38,6 @@ class UsersController < ApplicationController
respond_with(@users) respond_with(@users)
end end
def search
end
def show def show
@user = authorize User.find(params[:id]) @user = authorize User.find(params[:id])
respond_with(@user, methods: @user.full_attributes) do |format| respond_with(@user, methods: @user.full_attributes) do |format|
@@ -62,7 +59,7 @@ class UsersController < ApplicationController
end end
def create 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( @user = authorize User.new(
last_ip_addr: CurrentUser.ip_addr, last_ip_addr: CurrentUser.ip_addr,
@@ -80,7 +77,7 @@ class UsersController < ApplicationController
flash[:notice] = "Sign up failed" flash[:notice] = "Sign up failed"
elsif @user.email_address&.invalid?(:deliverable) elsif @user.email_address&.invalid?(:deliverable)
flash[:notice] = "Sign up failed: email address is invalid or doesn't exist" 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 elsif !@user.save
flash[:notice] = "Sign up failed: #{@user.errors.full_messages.join("; ")}" flash[:notice] = "Sign up failed: #{@user.errors.full_messages.join("; ")}"
else else

View File

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

View File

@@ -189,7 +189,7 @@ module ApplicationHelper
to_sentence(links, **options) to_sentence(links, **options)
end end
def link_to_user(user) def link_to_user(user, text = nil)
return "anonymous" if user.blank? return "anonymous" if user.blank?
user_class = "user user-#{user.level_string.downcase}" 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-post-uploader" if user.can_upload_free?
user_class += " user-banned" if user.is_banned? 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 } 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 end
def mod_link_to_user(user, positive_or_negative) def mod_link_to_user(user, positive_or_negative)
@@ -272,6 +273,7 @@ module ApplicationHelper
{ {
lang: "en", lang: "en",
class: "c-#{controller_param} a-#{action_param}", class: "c-#{controller_param} a-#{action_param}",
spellcheck: "false",
data: { data: {
controller: controller_param, controller: controller_param,
action: action_param, action: action_param,

View File

@@ -48,21 +48,10 @@ module PostsHelper
end end
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) def is_pool_selected?(pool)
return false if params.key?(:q) return false if params.key?(:q)
return false if params.key?(:favgroup_id) return false if params.key?(:favgroup_id)
return false if !params.key?(:pool_id) return false if !params.key?(:pool_id)
return params[:pool_id].to_i == pool.id return params[:pool_id].to_i == pool.id
end end
private
def nav_params_for(page)
query_params = params.except(:controller, :action, :id).merge(page: page).permit!
{ params: query_params }
end
end end

View File

@@ -3,12 +3,4 @@ module TagsHelper
return nil if tag.blank? return nil if tag.blank?
"tag-type-#{tag.category}" "tag-type-#{tag.category}"
end 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 end

View File

@@ -1,24 +1,4 @@
module UserUpgradesHelper module UserUpgradesHelper
def stripe_button(desc, cost, user)
html = %{
<form action="#{user_upgrade_path}" method="POST" class="stripe">
<input type="hidden" name="authenticity_token" value="#{form_authenticity_token}">
#{hidden_field_tag(:desc, desc)}
#{hidden_field_tag(:user_id, user.id)}
<script
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="#{Danbooru.config.stripe_publishable_key}"
data-name="#{Danbooru.config.canonical_app_name}"
data-description="#{desc}"
data-label="#{desc}"
data-amount="#{cost}">
</script>
</form>
}
raw(html)
end
def cents_to_usd(cents) def cents_to_usd(cents)
number_to_currency(cents / 100, precision: 0) number_to_currency(cents / 100, precision: 0)
end end

View File

@@ -3,16 +3,10 @@ import CurrentUser from './current_user'
let Autocomplete = {}; let Autocomplete = {};
/* eslint-disable */ /* eslint-disable */
Autocomplete.METATAGS = <%= PostQueryBuilder::METATAGS.to_json.html_safe %>;
Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.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 */ /* 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.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.MAX_RESULTS = 10;
Autocomplete.initialize_all = function() { Autocomplete.initialize_all = function() {
@@ -39,20 +33,20 @@ Autocomplete.initialize_all = function() {
this.initialize_tag_autocomplete(); this.initialize_tag_autocomplete();
this.initialize_mention_autocomplete($("form div.input.dtext textarea")); this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source); this.initialize_fields($('[data-autocomplete="tag"]'), "tag");
this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source); this.initialize_fields($('[data-autocomplete="artist"]'), "artist");
this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source); this.initialize_fields($('[data-autocomplete="pool"]'), "pool");
this.initialize_fields($('[data-autocomplete="user"]'), Autocomplete.user_source); this.initialize_fields($('[data-autocomplete="user"]'), "user");
this.initialize_fields($('[data-autocomplete="wiki-page"]'), Autocomplete.wiki_source); this.initialize_fields($('[data-autocomplete="wiki-page"]'), "wiki_page");
this.initialize_fields($('[data-autocomplete="favorite-group"]'), Autocomplete.favorite_group_source); this.initialize_fields($('[data-autocomplete="favorite-group"]'), "favorite_group");
this.initialize_fields($('[data-autocomplete="saved-search-label"]'), Autocomplete.saved_search_source); 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({ $fields.autocomplete({
source: async function(request, respond) { source: async function(request, respond) {
let results = await autocomplete(request.term); let results = await Autocomplete.autocomplete_source(request.term, type);
respond(results); respond(results);
}, },
}); });
@@ -84,7 +78,7 @@ Autocomplete.initialize_mention_autocomplete = function($fields) {
} }
if (name) { if (name) {
let results = await Autocomplete.user_source(name, "@"); let results = await Autocomplete.autocomplete_source(name, "mention");
resp(results); resp(results);
} }
} }
@@ -106,76 +100,18 @@ Autocomplete.initialize_tag_autocomplete = function() {
return false; return false;
}, },
source: async function(req, resp) { source: async function(req, resp) {
var query = Autocomplete.parse_query(req.term, this.element.get(0).selectionStart); let term = Autocomplete.current_term(this.element);
var metatag = query.metatag; let results = await Autocomplete.autocomplete_source(term, "tag_query");
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;
}
resp(results); resp(results);
} }
}); });
} }
Autocomplete.parse_query = function(text, caret) { Autocomplete.current_term = function($input) {
let before_caret_text = text.substring(0, caret); let query = $input.get(0).value;
let match = before_caret_text.match(Autocomplete.TERM_REGEX); let caret = $input.get(0).selectionStart;
let match = query.substring(0, caret).match(/\S*$/);
let operator = match[1]; return match[0];
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 };
}; };
// Update the input field with the item currently focused in the // 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); $link.append($post_count);
} }
if (item.type === "tag") { if (/^tag/.test(item.type)) {
$link.addClass("tag-type-" + item.category); $link.addClass("tag-type-" + item.category);
} else if (item.type === "user") { } else if (item.type === "user") {
var level_class = "user-" + item.level.toLowerCase(); var level_class = "user-" + item.level.toLowerCase();
@@ -256,7 +192,7 @@ Autocomplete.render_item = function(list, item) {
var $menu_item = $("<div/>").append($link); var $menu_item = $("<div/>").append($link);
var $list_item = $("<li/>").data("item.autocomplete", item).append($menu_item); var $list_item = $("<li/>").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 => { data_attributes.forEach(attr => {
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[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); return $list_item.appendTo(list);
}; };
Autocomplete.static_metatags = { Autocomplete.autocomplete_source = function(query, type) {
order: Autocomplete.ORDER_METATAGS, return $.getJSON("/autocomplete.json", {
status: ["any"].concat(Autocomplete.MISC_STATUSES), "search[query]": query,
rating: [ "search[type]": type,
"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 + "*",
"limit": Autocomplete.MAX_RESULTS "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() { $(document).ready(function() {

View File

@@ -3,7 +3,7 @@ import Cookie from './cookie'
$(function() { $(function() {
$("#hide-upgrade-account-notice").on("click.danbooru", function(e) { $("#hide-upgrade-account-notice").on("click.danbooru", function(e) {
$("#upgrade-account-notice").hide(); $("#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(); e.preventDefault();
}); });
@@ -17,7 +17,7 @@ $(function() {
$("#hide-verify-account-notice").on("click.danbooru", function(e) { $("#hide-verify-account-notice").on("click.danbooru", function(e) {
$("#verify-account-notice").hide(); $("#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(); e.preventDefault();
}); });

View File

@@ -1,27 +1,17 @@
import Utility from "./utility";
let Cookie = {}; let Cookie = {};
Cookie.put = function(name, value, days) { Cookie.put = function(name, value, max_age_in_seconds = 60 * 60 * 24 * 365 * 20) {
var expires = ""; let cookie = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax;`;
if (days !== "session") {
if (!days) {
days = 365;
}
var date = new Date(); if (max_age_in_seconds) {
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); cookie += ` Max-Age=${max_age_in_seconds};`
expires = "expires=" + date.toGMTString() + "; ";
} }
var new_val = name + "=" + encodeURIComponent(value) + "; " + expires + "path=/"; if (location.protocol === "https:") {
if (document.cookie.length < (4090 - new_val.length)) { cookie += " Secure;";
document.cookie = new_val;
return true;
} else {
Utility.error("You have too many cookies on this site. Consider deleting them all.")
return false;
} }
document.cookie = cookie;
} }
Cookie.raw_get = function(name) { Cookie.raw_get = function(name) {

View File

@@ -118,6 +118,17 @@ table tfoot {
margin-top: 2em; margin-top: 2em;
} }
details {
border-bottom: 1px solid var(--details-border);
summary {
cursor: pointer;
user-select: none;
outline: none;
line-height: 2em;
}
}
.fineprint { .fineprint {
color: var(--muted-text-color); color: var(--muted-text-color);
font-style: italic; font-style: italic;

View File

@@ -35,9 +35,12 @@
--quick-search-form-background: var(--body-background-color); --quick-search-form-background: var(--body-background-color);
--user-upgrade-basic-background-color: #F5F5FF;
--user-upgrade-gold-background-color: #FFF380; --user-upgrade-gold-background-color: #FFF380;
--user-upgrade-platinum-background-color: #EEE; --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-header-border: 2px solid #666;
--table-row-border: 1px solid #CCC; --table-row-border: 1px solid #CCC;
@@ -72,6 +75,8 @@
--comment-sticky-background-color: var(--subnav-menu-background-color); --comment-sticky-background-color: var(--subnav-menu-background-color);
--details-border: #DDD;
--post-tooltip-background-color: var(--body-background-color); --post-tooltip-background-color: var(--body-background-color);
--post-tooltip-border-color: hsla(210, 100%, 3%, 0.15); --post-tooltip-border-color: hsla(210, 100%, 3%, 0.15);
--post-tooltip-box-shadow: 0 4px 14px -2px hsla(210, 100%, 3%, 0.10); --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-selected-background-color: var(--subnav-menu-background-color);
--autocomplete-border: 1px solid #CCC; --autocomplete-border: 1px solid #CCC;
--autocomplete-arrow-color: var(--text-color); --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-added-color: green;
--diff-list-removed-color: red; --diff-list-removed-color: red;
@@ -157,7 +163,8 @@
--bulk-update-request-failed-color: red; --bulk-update-request-failed-color: red;
--login-link-color: #E00; --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-background: var(--body-background-color);
--jquery-ui-widget-content-text-color: var(--text-color); --jquery-ui-widget-content-text-color: var(--text-color);
@@ -201,6 +208,9 @@
--user-member-color: var(--link-color); --user-member-color: var(--link-color);
--user-banned-color: black; --user-banned-color: black;
--user-verified-email-color: #0A0;
--user-unverified-email-color: #F80;
--news-updates-background: #EEE; --news-updates-background: #EEE;
--news-updates-border: 2px solid #666; --news-updates-border: 2px solid #666;
@@ -252,6 +262,7 @@ body[data-current-user-theme="dark"] {
--subnav-menu-background-color: var(--grey-2); --subnav-menu-background-color: var(--grey-2);
--responsive-menu-background-color: var(--grey-3); --responsive-menu-background-color: var(--grey-3);
--footer-border: 1px solid var(--grey-3); --footer-border: 1px solid var(--grey-3);
--details-border: var(--grey-3);
--table-header-border: 2px solid var(--grey-3); --table-header-border: 2px solid var(--grey-3);
--table-even-row-background: var(--grey-2); --table-even-row-background: var(--grey-2);
@@ -291,6 +302,9 @@ body[data-current-user-theme="dark"] {
--user-moderator-color: var(--green-1); --user-moderator-color: var(--green-1);
--user-admin-color: var(--red-1); --user-admin-color: var(--red-1);
--user-verified-email-color: var(--green-1);
--user-unverified-email-color: var(--yellow-1);
/* misc specific colors */ /* misc specific colors */
--autocomplete-selected-background-color: var(--grey-3); --autocomplete-selected-background-color: var(--grey-3);
--autocomplete-border: 1px solid var(--grey-4); --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-foreground-color: var(--link-color);
--uploads-dropzone-progress-bar-background-color: var(--link-hover-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-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-other-name-background-color: var(--grey-3);
--wiki-page-versions-diff-del-background: var(--red-0); --wiki-page-versions-diff-del-background: var(--red-0);

View File

@@ -21,4 +21,15 @@
.autocomplete-arrow { .autocomplete-arrow {
color: var(--autocomplete-arrow-color); 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;
}
} }

View File

@@ -97,7 +97,6 @@ div.prose {
div.expandable-content { div.expandable-content {
display: none; display: none;
padding: 0.4em; padding: 0.4em;
border-top: 1px solid var(--dtext-expand-border-color);
> :last-child { > :last-child {
margin-bottom: 0; margin-bottom: 0;

View File

@@ -320,3 +320,12 @@
local("Anarchy"), local("Anarchy"),
url("../../../../../public/fonts/Anarchy.ttf") format("truetype"); 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");
}

View File

@@ -1,4 +1,8 @@
body[data-current-user-style-usernames="true"] { body[data-current-user-style-usernames="true"] {
a.user-owner {
color: var(--user-admin-color);
}
a.user-admin { a.user-admin {
color: var(--user-admin-color); color: var(--user-admin-color);
} }

View File

@@ -27,6 +27,7 @@
margin-right: 0.25em; margin-right: 0.25em;
border-radius: 3px; 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-admin { background-color: var(--user-admin-color); }
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); } &.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); } &.user-tooltip-badge-approver { background-color: var(--user-builder-color); }

View File

@@ -1,43 +1,66 @@
div#c-user-upgrades { div#c-user-upgrades {
div#a-new { div#a-new {
form.stripe { margin: 0 auto;
display: inline;
}
div.section { * {
margin-bottom: 2em;
}
div#feature-comparison {
overflow: hidden;
margin-bottom: 1em; margin-bottom: 1em;
}
table { h1 {
width: 100%; text-align: center;
}
colgroup { .login-button, form.button_to input[type="submit"] {
width: 10em; display: inline-block;
}
colgroup#gold { color: var(--user-upgrade-button-text-color);
background-color: var(--user-upgrade-gold-background-color); background-color: var(--user-upgrade-button-background-color);
}
colgroup#platinum { border: none;
background-color: var(--user-upgrade-platinum-background-color); border-radius: 4px;
} padding: 0.75em;
td, th { 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;
text-align: center; 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);
vertical-align: top;
padding: 0.5em 0;
}
tbody { &:hover:not([disabled]) {
tr:hover { background-color: var(--user-upgrade-button-hover-background-color);
background-color: var(--user-upgrade-table-row-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;
} }
} }
} }

View File

@@ -30,6 +30,14 @@ div#c-users {
p { p {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.user-verified-email-icon {
color: var(--user-verified-email-color);
}
.user-unverified-email-icon {
color: var(--user-unverified-email-color);
}
} }
} }

View File

@@ -8,6 +8,7 @@ module ArtistFinder
"www.artstation.com", # http://www.artstation.com/serafleur/ "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 %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 "ask.fm", # http://ask.fm/mikuroko_396
"baraag.net",
"bcyimg.com", "bcyimg.com",
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg "bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
"bcy.net", "bcy.net",
@@ -60,6 +61,7 @@ module ArtistFinder
"iwara.tv/users", # http://ecchi.iwara.tv/users/marumega "iwara.tv/users", # http://ecchi.iwara.tv/users/marumega
"kym-cdn.com", "kym-cdn.com",
"livedoor.blogimg.jp", "livedoor.blogimg.jp",
"blog.livedoor.jp", # http://blog.livedoor.jp/ac370ml
"monappy.jp", "monappy.jp",
"monappy.jp/u", # https://monappy.jp/u/abara_bone "monappy.jp/u", # https://monappy.jp/u/abara_bone
"mstdn.jp", # https://mstdn.jp/@oneb "mstdn.jp", # https://mstdn.jp/@oneb
@@ -83,8 +85,8 @@ module ArtistFinder
"pixiv.net", # https://www.pixiv.net/member.php?id=10442390 "pixiv.net", # https://www.pixiv.net/member.php?id=10442390
"pixiv.net/stacc", # https://www.pixiv.net/stacc/aaaninja2013 "pixiv.net/stacc", # https://www.pixiv.net/stacc/aaaninja2013
"pixiv.net/fanbox/creator", # https://www.pixiv.net/fanbox/creator/310630 "pixiv.net/fanbox/creator", # https://www.pixiv.net/fanbox/creator/310630
"pixiv.net/users", # https://www.pixiv.net/users/555603 %r{pixiv.net/(?:en/)?users}i, # https://www.pixiv.net/users/555603
"pixiv.net/en/users", # https://www.pixiv.net/en/users/555603 %r{pixiv.net/(?:en/)?artworks}i, # https://www.pixiv.net/en/artworks/85241178
"i.pximg.net", "i.pximg.net",
"plurk.com", # http://www.plurk.com/a1amorea1a1 "plurk.com", # http://www.plurk.com/a1amorea1a1
"privatter.net", "privatter.net",

View File

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

View File

@@ -1,5 +1,11 @@
class BulkUpdateRequestProcessor class BulkUpdateRequestProcessor
# Maximum tag size allowed by the rename command before an alias must be used.
MAXIMUM_RENAME_COUNT = 200 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 MAXIMUM_SCRIPT_LENGTH = 100
include ActiveModel::Validations 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 = TagAlias.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1])
tag_alias.save(context: validation_context) tag_alias.save(context: validation_context)
if tag_alias.errors.present? 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 end
when :create_implication when :create_implication
tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], status: "active") tag_implication = TagImplication.new(creator: User.system, antecedent_name: args[0], consequent_name: args[1], status: "active")
tag_implication.save(context: validation_context) tag_implication.save(context: validation_context)
if tag_implication.errors.present? 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 end
when :remove_alias when :remove_alias
tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1]) tag_alias = TagAlias.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_alias.nil? 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 else
tag_alias.update(status: "deleted") tag_alias.update(status: "deleted")
end end
@@ -76,7 +82,7 @@ class BulkUpdateRequestProcessor
when :remove_implication when :remove_implication
tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1]) tag_implication = TagImplication.active.find_by(antecedent_name: args[0], consequent_name: args[1])
if tag_implication.nil? 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 else
tag_implication.update(status: "deleted") tag_implication.update(status: "deleted")
end end
@@ -84,22 +90,22 @@ class BulkUpdateRequestProcessor
when :change_category when :change_category
tag = Tag.find_by_name(args[0]) tag = Tag.find_by_name(args[0])
if tag.nil? 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 end
when :rename when :rename
tag = Tag.find_by_name(args[0]) tag = Tag.find_by_name(args[0])
if tag.nil? 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 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 end
when :mass_update, :nuke when :mass_update, :nuke
# okay # okay
when :invalid_line when :invalid_line
errors[:base] << "Invalid line: #{args[0]}" errors.add(:base, "Invalid line: #{args[0]}")
else else
# should never happen # should never happen
@@ -113,7 +119,7 @@ class BulkUpdateRequestProcessor
def validate_script_length def validate_script_length
if commands.size > MAXIMUM_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
end end
@@ -212,11 +218,18 @@ class BulkUpdateRequestProcessor
end.join("\n") end.join("\n")
end 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) def self.is_tag_move_allowed?(antecedent_name, consequent_name)
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name)) antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_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)) && antecedent_allowed = antecedent_tag.present? && antecedent_tag.artist? && antecedent_tag.post_count < MAXIMUM_BUILDER_MOVE_COUNT
(consequent_tag.blank? || consequent_tag.empty? || (consequent_tag.artist? && consequent_tag.post_count <= 100)) 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
end end

View File

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

View File

@@ -10,12 +10,13 @@ module Searchable
1 + params.values.map { |v| parameter_hash?(v) ? parameter_depth(v) : 1 }.max 1 + params.values.map { |v| parameter_hash?(v) ? parameter_depth(v) : 1 }.max
end end
def negate(kind = :nand) def negate_relation
unscoped.where(all.where_clause.invert(kind).ast) unscoped.where(all.where_clause.invert.ast)
end end
# XXX hacky method to AND two relations together. # 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 = all
q = q.where(relation.where_clause.ast) if relation.where_clause.present? 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? q = q.joins(relation.joins_values + q.joins_values) if relation.joins_values.present?
@@ -52,7 +53,7 @@ module Searchable
end end
def where_iequals(attr, value) def where_iequals(attr, value)
where_ilike(attr, value.gsub(/\\/, '\\\\').gsub(/\*/, '\*')) where_ilike(attr, value.escape_wildcards)
end end
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP # https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
@@ -101,16 +102,11 @@ module Searchable
where_operator(qualified_column, *range) where_operator(qualified_column, *range)
end end
def search_boolean_attribute(attribute, params) def search_boolean_attribute(attr, params)
return all unless params.key?(attribute) if params[attr].present?
boolean_attribute_matches(attr, params[attr])
value = params[attribute].to_s
if value.truthy?
where(attribute => true)
elsif value.falsy?
where(attribute => false)
else else
raise ArgumentError, "value must be truthy or falsy" all
end end
end end
@@ -132,6 +128,18 @@ module Searchable
where_operator(qualified_column, *range) where_operator(qualified_column, *range)
end 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") def text_attribute_matches(attribute, value, index_column: nil, ts_config: "english")
return all unless value.present? return all unless value.present?
@@ -182,7 +190,7 @@ module Searchable
when :boolean when :boolean
search_boolean_attribute(name, params) search_boolean_attribute(name, params)
when :integer, :datetime when :integer, :datetime
numeric_attribute_matches(name, params[name]) search_numeric_attribute(name, params)
when :inet when :inet
search_inet_attribute(name, params) search_inet_attribute(name, params)
when :enum when :enum
@@ -195,6 +203,26 @@ module Searchable
end end
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) def search_text_attribute(attr, params)
if params[attr].present? if params[attr].present?
where(attr => params[attr]) where(attr => params[attr])
@@ -385,14 +413,6 @@ module Searchable
where(id: ids).order(Arel.sql(order_clause.join(', '))) where(id: ids).order(Arel.sql(order_clause.join(', ')))
end 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 private
def qualified_column_for(attr) def qualified_column_for(attr)

View File

@@ -40,6 +40,14 @@ class CurrentUser
RequestStore[:current_ip_addr] = ip_addr RequestStore[:current_ip_addr] = ip_addr
end end
def self.country
RequestStore[:country]
end
def self.country=(country)
RequestStore[:country] = country
end
def self.root_url def self.root_url
RequestStore[:current_root_url] || "https://#{Danbooru.config.hostname}" RequestStore[:current_root_url] || "https://#{Danbooru.config.hostname}"
end end

View File

@@ -109,11 +109,11 @@ class DText
end end
if obj.is_approved? 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? 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? 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 end
end end

View File

@@ -1,3 +1,4 @@
require "danbooru/http/application_client"
require "danbooru/http/html_adapter" require "danbooru/http/html_adapter"
require "danbooru/http/xml_adapter" require "danbooru/http/xml_adapter"
require "danbooru/http/cache" require "danbooru/http/cache"

View File

@@ -1,4 +1,6 @@
class DanbooruLogger class DanbooruLogger
HEADERS = %w[referer sec-fetch-dest sec-fetch-mode sec-fetch-site sec-fetch-user]
def self.info(message, params = {}) def self.info(message, params = {})
Rails.logger.info(message) Rails.logger.info(message)
@@ -22,25 +24,52 @@ class DanbooruLogger
end end
def self.add_session_attributes(request, session, user) def self.add_session_attributes(request, session, user)
request_params = request.parameters.with_indifferent_access.except(:controller, :action) add_attributes("request", { path: request.path })
session_params = session.to_h.with_indifferent_access.slice(:session_id, :started_at) add_attributes("request.headers", header_params(request))
user_params = { id: user&.id, name: user&.name, level: user&.level_string, ip: request.remote_ip, safe_mode: CurrentUser.safe_mode? } 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) def self.header_params(request)
add_attributes("session.params", session_params) headers = request.headers.to_h.select { |header, value| header.match?(/\AHTTP_/) }
add_attributes("user", user_params) 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 end
def self.add_attributes(prefix, hash) def self.add_attributes(prefix, hash)
return unless defined?(::NewRelic)
attributes = flatten_hash(hash).transform_keys { |key| "#{prefix}.#{key}" } 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)) } 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 end
private_class_method 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 } } }) # flatten_hash({ foo: { bar: { baz: 42 } } })
# => { "foo.bar.baz" => 42 } # => { "foo.bar.baz" => 42 }
def self.flatten_hash(hash) def self.flatten_hash(hash)

View File

@@ -16,7 +16,7 @@ class DtextInput < SimpleForm::Inputs::Base
t = template t = template
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) 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] if options[:inline]
t.concat @builder.text_field(attribute_name, merged_input_options) t.concat @builder.text_field(attribute_name, merged_input_options)
else else

View File

@@ -3,6 +3,9 @@ require 'resolv'
module EmailValidator module EmailValidator
module_function 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_DOTS = %w[gmail.com]
IGNORE_PLUS_ADDRESSING = %w[gmail.com hotmail.com outlook.com live.com] IGNORE_PLUS_ADDRESSING = %w[gmail.com hotmail.com outlook.com live.com]
IGNORE_MINUS_ADDRESSING = %w[yahoo.com] IGNORE_MINUS_ADDRESSING = %w[yahoo.com]
@@ -80,10 +83,17 @@ module EmailValidator
"#{name}@#{domain}" "#{name}@#{domain}"
end end
def nondisposable?(address) def is_valid?(address)
return true if Danbooru.config.email_domain_verification_list.blank? 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 = 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 end
def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3) def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3)

View File

@@ -3,6 +3,9 @@ require "strscan"
class PostQueryBuilder class PostQueryBuilder
extend Memoist extend Memoist
# How many tags a `blah*` search should match.
MAX_WILDCARD_TAGS = 100
COUNT_METATAGS = %w[ COUNT_METATAGS = %w[
comment_count deleted_comment_count active_comment_count comment_count deleted_comment_count active_comment_count
note_count deleted_note_count active_note_count note_count deleted_note_count active_note_count
@@ -77,9 +80,9 @@ class PostQueryBuilder
optional_tags = optional_tags.map(&:name) optional_tags = optional_tags.map(&:name)
required_tags = required_tags.map(&:name) required_tags = required_tags.map(&:name)
negated_tags += negated_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) } 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) } 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 << "!(#{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? 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) def metatags_match(metatags, relation)
metatags.each do |metatag| metatags.each do |metatag|
clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted) clause = metatag_matches(metatag.name, metatag.value, quoted: metatag.quoted)
clause = clause.negate if metatag.negated clause = clause.negate_relation if metatag.negated
relation = relation.and(clause) relation = relation.and_relation(clause)
end end
relation relation
@@ -390,7 +393,8 @@ class PostQueryBuilder
favuser = User.find_by_name(username) favuser = User.find_by_name(username)
if favuser.present? && Pundit.policy!([current_user, nil], favuser).can_see_favorites? 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 else
Post.none Post.none
end end
@@ -399,8 +403,8 @@ class PostQueryBuilder
def ordfav_matches(username) def ordfav_matches(username)
user = User.find_by_name(username) user = User.find_by_name(username)
if user.present? if user.present? && Pundit.policy!([current_user, nil], user).can_see_favorites?
favorites_include(username).joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC") Post.joins(:favorites).merge(Favorite.for_user(user.id)).order("favorites.id DESC")
else else
Post.none Post.none
end end
@@ -985,6 +989,11 @@ class PostQueryBuilder
def is_wildcard_search? def is_wildcard_search?
is_single_tag? && tags.first.wildcard is_single_tag? && tags.first.wildcard
end end
def simple_tag
return nil if !is_simple_tag?
Tag.find_by_name(tags.first.name)
end
end end
memoize :split_query, :normalized_query memoize :split_query, :normalized_query

View File

@@ -187,16 +187,19 @@ module PostSets
RelatedTagCalculator.frequent_tags_for_post_array(posts).take(MAX_SIDEBAR_TAGS) RelatedTagCalculator.frequent_tags_for_post_array(posts).take(MAX_SIDEBAR_TAGS)
end 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 def wildcard_tags
Tag.wildcard_matches(tag_string) Tag.wildcard_matches(tag_string).limit(PostQueryBuilder::MAX_WILDCARD_TAGS).pluck(:name)
end end
def saved_search_tags 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 end
def tag_set_presenter def tag_set_presenter
@tag_set_presenter ||= TagSetPresenter.new(related_tags.take(MAX_SIDEBAR_TAGS)) @tag_set_presenter ||= TagSetPresenter.new(related_tags)
end end
def tag_list_html(**options) def tag_list_html(**options)

View File

@@ -71,9 +71,12 @@ class RelatedTagQuery
end end
def other_wiki_pages 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 copyright_other_wiki_pages
elsif Tag.category_for(query) == Tag.categories.general elsif tag.general?
general_other_wiki_pages general_other_wiki_pages
else else
[] []

11
app/logical/routes.rb Normal file
View File

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

View File

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

View File

@@ -34,6 +34,7 @@ class SessionLoader
update_last_logged_in_at update_last_logged_in_at
update_last_ip_addr update_last_ip_addr
set_time_zone set_time_zone
set_country
set_safe_mode set_safe_mode
initialize_session_cookies initialize_session_cookies
CurrentUser.user.unban! if CurrentUser.user.ban_expired? CurrentUser.user.unban! if CurrentUser.user.ban_expired?
@@ -87,7 +88,7 @@ class SessionLoader
def update_last_logged_in_at def update_last_logged_in_at
return if CurrentUser.is_anonymous? 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) CurrentUser.user.update_attribute(:last_logged_in_at, Time.now)
end end
@@ -101,6 +102,12 @@ class SessionLoader
Time.zone = CurrentUser.user.time_zone Time.zone = CurrentUser.user.time_zone
end 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 def set_safe_mode
safe_mode = request.host.match?(/safebooru/i) || params[:safe_mode].to_s.truthy? || CurrentUser.user.enable_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 CurrentUser.safe_mode = safe_mode

View File

@@ -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| text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match|
pixiv_id = $1 pixiv_id = $1
%(pixiv ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}]) %(pixiv ##{pixiv_id} "»":[#{Routes.posts_path(tags: "pixiv:#{pixiv_id}")}])
end end
text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match| text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match|
member_id = $1 member_id = $1
profile_url = "https://www.pixiv.net/users/#{member_id}" 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 end
text = text.gsub(/\r\n|\r|\n/, "<br>") text = text.gsub(/\r\n|\r|\n/, "<br>")

View File

@@ -90,6 +90,10 @@ module Sources
image_urls.map { |img| img.gsub(%r{.cn/\w+/(\w+)}, '.cn/orj360/\1') } image_urls.map { |img| img.gsub(%r{.cn/\w+/(\w+)}, '.cn/orj360/\1') }
end end
def headers
{ "Referer" => "https://weibo.com" }
end
def page_url def page_url
if api_response.present? if api_response.present?
artist_id = api_response["user"]["id"] artist_id = api_response["user"]["id"]

View File

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

View File

@@ -1,38 +1,40 @@
class TagNameValidator < ActiveModel::EachValidator class TagNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) 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/ when /\A_*\z/
record.errors[attribute] << "'#{value}' cannot be blank" record.errors.add(attribute, "'#{value}' cannot be blank")
when /\*/ when /\*/
record.errors[attribute] << "'#{value}' cannot contain asterisks ('*')" record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
when /,/ when /,/
record.errors[attribute] << "'#{value}' cannot contain commas (',')" record.errors.add(attribute, "'#{value}' cannot contain commas (',')")
when /\A~/ when /\A[-~_`%){}\]\/]/
record.errors[attribute] << "'#{value}' cannot begin with a tilde ('~')" record.errors.add(attribute, "'#{value}' cannot begin with a '#{value[0]}'")
when /\A-/
record.errors[attribute] << "'#{value}' cannot begin with a dash ('-')"
when /\A_/
record.errors[attribute] << "'#{value}' cannot begin with an underscore"
when /_\z/ when /_\z/
record.errors[attribute] << "'#{value}' cannot end with an underscore" record.errors.add(attribute, "'#{value}' cannot end with an underscore")
when /__/ when /__/
record.errors[attribute] << "'#{value}' cannot contain consecutive underscores" record.errors.add(attribute, "'#{value}' cannot contain consecutive underscores")
when /[^[:graph:]]/ when /[^[:graph:]]/
record.errors[attribute] << "'#{value}' cannot contain non-printable characters" record.errors.add(attribute, "'#{value}' cannot contain non-printable characters")
when /[^[:ascii:]]/ 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 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 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" 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 when /\A(.+)_\(cosplay\)\z/i
tag_name = TagAlias.to_aliased([$1]).first tag_name = TagAlias.to_aliased([$1]).first
tag = Tag.find_by_name(tag_name) tag = Tag.find_by_name(tag_name)
if tag.present? && !tag.empty? && !tag.character? 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 end
end end

View File

@@ -11,7 +11,7 @@ class UploadService
end end
def comment_replacement_message(post, replacement) 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 end
def replacement_message(post, replacement) def replacement_message(post, replacement)

View File

@@ -61,11 +61,11 @@ class UserDeletion
def validate_deletion def validate_deletion
if !user.authenticate_password(password) if !user.authenticate_password(password)
errors[:base] << "Password is incorrect" errors.add(:base, "Password is incorrect")
end end
if user.level >= User::Levels::ADMIN if user.is_admin?
errors[:base] << "Admins cannot delete their account" errors.add(:base, "Admins cannot delete their account")
end end
end end
end end

View File

@@ -2,10 +2,10 @@ class UserNameValidator < ActiveModel::EachValidator
def validate_each(rec, attr, value) def validate_each(rec, attr, value)
name = value name = value
rec.errors[attr] << "already exists" if User.find_by_name(name).present? rec.errors.add(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.add(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.add(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.add(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, "is not allowed") if name =~ Regexp.union(Danbooru.config.user_name_blacklist)
end end
end end

View File

@@ -1,33 +1,27 @@
class UserPromotion 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 @user = user
@promoter = promoter @promoter = promoter
@new_level = new_level @new_level = new_level.to_i
@options = options @can_upload_free = can_upload_free
@can_approve_posts = can_approve_posts
end end
def promote! def promote!
validate validate!
@old_can_approve_posts = user.can_approve_posts? @old_can_approve_posts = user.can_approve_posts?
@old_can_upload_free = user.can_upload_free? @old_can_upload_free = user.can_upload_free?
user.level = new_level 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) create_user_feedback
user.can_approve_posts = options[:can_approve_posts] create_dmail
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_mod_actions create_mod_actions
user.save user.save
@@ -37,28 +31,28 @@ class UserPromotion
def create_mod_actions def create_mod_actions
if old_can_approve_posts != user.can_approve_posts? 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 end
if old_can_upload_free != user.can_upload_free? 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 end
if user.level_changed? if user.level_changed?
category = options[:is_upgrade] ? :user_account_upgrade : :user_level_change ModAction.log(%{"#{user.name}":#{Routes.user_path(user)} level changed #{user.level_string_was} -> #{user.level_string}}, :user_level_change, promoter)
ModAction.log(%{"#{user.name}":/users/#{user.id} level changed #{user.level_string_was} -> #{user.level_string}}, category)
end end
end end
def validate def validate!
# admins can do anything if !promoter.is_moderator?
return if promoter.is_admin? raise User::PrivilegeError, "You can't promote or demote other users"
elsif promoter == user
# can't promote/demote moderators raise User::PrivilegeError, "You can't promote or demote yourself"
raise User::PrivilegeError if user.is_moderator? elsif new_level >= promoter.level
raise User::PrivilegeError, "You can't promote other users to your rank or above"
# can't promote to admin elsif user.level >= promoter.level
raise User::PrivilegeError if new_level.to_i >= User::Levels::ADMIN raise User::PrivilegeError, "You can't promote or demote other users at your rank or above"
end
end end
def build_messages def build_messages

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
class UserMailer < ApplicationMailer class UserMailer < ApplicationMailer
add_template_helper ApplicationHelper helper :application
add_template_helper UsersHelper helper :users
def dmail_notice(dmail) def dmail_notice(dmail)
@dmail = dmail @dmail = dmail

View File

@@ -3,6 +3,7 @@ class ApplicationRecord < ActiveRecord::Base
include Deletable include Deletable
include Mentionable include Mentionable
include Normalizable
extend HasBitFlags extend HasBitFlags
extend Searchable extend Searchable
@@ -93,10 +94,6 @@ class ApplicationRecord < ActiveRecord::Base
concerning :SearchMethods do concerning :SearchMethods do
class_methods do class_methods do
def searchable_includes
[]
end
def model_restriction(table) def model_restriction(table)
table.project(1) table.project(1)
end end

View File

@@ -156,7 +156,7 @@ class Artist < ApplicationRecord
return unless !is_deleted? && name_changed? && tag.present? return unless !is_deleted? && name_changed? && tag.present?
if tag.category_name != "Artist" && !tag.empty? 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
end end
@@ -203,6 +203,10 @@ class Artist < ApplicationRecord
end end
module SearchMethods module SearchMethods
def name_matches(query)
where_like(:name, normalize_name(query))
end
def any_other_name_matches(regex) def any_other_name_matches(regex)
where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex)) where(id: Artist.from("unnest(other_names) AS other_name").where_regex("other_name", regex))
end end
@@ -246,9 +250,7 @@ class Artist < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :other_names, :urls, :wiki_page, :tag_alias, :tag)
q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :other_names)
if params[:any_other_name_like] if params[:any_other_name_like]
q = q.any_other_name_like(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)) super.where(table[:is_deleted].eq(false))
end end
def self.searchable_includes
[:urls, :wiki_page, :tag_alias, :tag]
end
def self.available_includes def self.available_includes
[:members, :urls, :wiki_page, :tag_alias, :tag] [:members, :urls, :wiki_page, :tag_alias, :tag]
end end

View File

@@ -31,9 +31,7 @@ class ArtistCommentary < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post)
q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
if params[:text_matches].present? if params[:text_matches].present?
q = q.text_matches(params[:text_matches]) q = q.text_matches(params[:text_matches])
@@ -146,10 +144,6 @@ class ArtistCommentary < ApplicationRecord
extend SearchMethods extend SearchMethods
include VersionMethods include VersionMethods
def self.searchable_includes
[:post]
end
def self.available_includes def self.available_includes
[:post] [:post]
end end

View File

@@ -12,8 +12,7 @@ class ArtistCommentaryVersion < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :original_title, :original_description, :translated_title, :translated_description, :post, :updater)
q = q.search_attributes(params, :original_title, :original_description, :translated_title, :translated_description)
if params[:text_matches].present? if params[:text_matches].present?
q = q.text_matches(params[:text_matches]) q = q.text_matches(params[:text_matches])
@@ -56,10 +55,6 @@ class ArtistCommentaryVersion < ApplicationRecord
self[field].strip.empty? && (previous.nil? || previous[field].strip.empty?) self[field].strip.empty? && (previous.nil? || previous[field].strip.empty?)
end end
def self.searchable_includes
[:post, :updater]
end
def self.available_includes def self.available_includes
[:post, :updater] [:post, :updater]
end end

View File

@@ -40,9 +40,7 @@ class ArtistUrl < ApplicationRecord
end end
def self.search(params = {}) def self.search(params = {})
q = super q = search_attributes(params, :id, :created_at, :updated_at, :url, :normalized_url, :is_active, :artist)
q = q.search_attributes(params, :url, :normalized_url, :is_active)
q = q.url_matches(params[:url_matches]) q = q.url_matches(params[:url_matches])
q = q.normalized_url_matches(params[:normalized_url_matches]) q = q.normalized_url_matches(params[:normalized_url_matches])
@@ -113,11 +111,11 @@ class ArtistUrl < ApplicationRecord
end end
def validate_scheme(uri) 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 end
def validate_hostname(uri) 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 end
def validate_url_format def validate_url_format
@@ -125,11 +123,7 @@ class ArtistUrl < ApplicationRecord
validate_scheme(uri) validate_scheme(uri)
validate_hostname(uri) validate_hostname(uri)
rescue Addressable::URI::InvalidURIError => error rescue Addressable::URI::InvalidURIError => error
errors[:url] << "'#{uri}' is malformed: #{error}" errors.add(:url, "'#{uri}' is malformed: #{error}")
end
def self.searchable_includes
[:artist]
end end
def self.available_includes def self.available_includes

View File

@@ -7,9 +7,7 @@ class ArtistVersion < ApplicationRecord
module SearchMethods module SearchMethods
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names, :updater, :artist)
q = q.search_attributes(params, :is_deleted, :is_banned, :name, :group_name, :urls, :other_names)
q = q.text_attribute_matches(:name, params[:name_matches]) q = q.text_attribute_matches(:name, params[:name_matches])
q = q.text_attribute_matches(:group_name, params[:group_name_matches]) q = q.text_attribute_matches(:group_name, params[:group_name_matches])
@@ -105,10 +103,6 @@ class ArtistVersion < ApplicationRecord
end end
end end
def self.searchable_includes
[:updater, :artist]
end
def self.available_includes def self.available_includes
[:updater, :artist] [:updater, :artist]
end end

View File

@@ -20,9 +20,7 @@ class Ban < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :expires_at, :reason, :user, :banner)
q = q.search_attributes(params, :expires_at, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches]) q = q.text_attribute_matches(:reason, params[:reason_matches])
q = q.expired if params[:expired].to_s.truthy? q = q.expired if params[:expired].to_s.truthy?
@@ -45,7 +43,7 @@ class Ban < ApplicationRecord
end end
def validate_user_is_bannable 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 end
def update_user_on_create def update_user_on_create
@@ -89,10 +87,6 @@ class Ban < ApplicationRecord
ModAction.log(%{Unbanned <@#{user_name}>}, :user_unban) ModAction.log(%{Unbanned <@#{user_name}>}, :user_unban)
end end
def self.searchable_includes
[:user, :banner]
end
def self.available_includes def self.available_includes
[:user, :banner] [:user, :banner]
end end

View File

@@ -31,9 +31,7 @@ class BulkUpdateRequest < ApplicationRecord
end end
def search(params = {}) def search(params = {})
q = super q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags, :user, :forum_topic, :forum_post, :approver)
q = q.search_attributes(params, :script, :tags)
q = q.text_attribute_matches(:script, params[:script_matches]) q = q.text_attribute_matches(:script, params[:script_matches])
if params[:status].present? if params[:status].present?
@@ -91,13 +89,13 @@ class BulkUpdateRequest < ApplicationRecord
end end
def bulk_update_request_link 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
end end
def validate_script def validate_script
if processor.invalid?(:request) if processor.invalid?(:request)
errors[:base] << processor.errors.full_messages.join("; ") errors.add(:base, processor.errors.full_messages.join("; "))
end end
end end
@@ -128,10 +126,6 @@ class BulkUpdateRequest < ApplicationRecord
status == "rejected" status == "rejected"
end end
def self.searchable_includes
[:user, :forum_topic, :forum_post, :approver]
end
def self.available_includes def self.available_includes
[:user, :forum_topic, :forum_post, :approver] [:user, :forum_topic, :forum_post, :approver]
end end

View File

@@ -21,14 +21,12 @@ class Comment < ApplicationRecord
mentionable( mentionable(
:message_field => :body, :message_field => :body,
:title => ->(user_name) {"#{creator.name} mentioned you in a comment on post ##{post_id}"}, :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 module SearchMethods
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score, :post, :creator, :updater)
q = q.search_attributes(params, :is_deleted, :is_sticky, :do_not_bump_post, :body, :score)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index) q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
case params[:order] case params[:order]
@@ -139,10 +137,6 @@ class Comment < ApplicationRecord
DText.quote(body, creator.name) DText.quote(body, creator.name)
end end
def self.searchable_includes
[:post, :creator, :updater]
end
def self.available_includes def self.available_includes
[:post, :creator, :updater] [:post, :creator, :updater]
end end

View File

@@ -19,14 +19,13 @@ class CommentVote < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :score, :comment, :user)
q = q.search_attributes(params, :score)
q.apply_default_order(params) q.apply_default_order(params)
end end
def validate_comment_can_be_down_voted def validate_comment_can_be_down_voted
if is_positive? && comment.creator == CurrentUser.user 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
end end
@@ -38,10 +37,6 @@ class CommentVote < ApplicationRecord
score == -1 score == -1
end end
def self.searchable_includes
[:comment, :user]
end
def self.available_includes def self.available_includes
[:comment, :user] [:comment, :user]
end end

View File

@@ -98,9 +98,7 @@ class Dmail < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_read, :is_deleted, :title, :body, :to, :from)
q = q.search_attributes(params, :is_read, :is_deleted, :title, :body)
q = q.text_attribute_matches(:title, params[:title_matches]) q = q.text_attribute_matches(:title, params[:title_matches])
q = q.text_attribute_matches(:body, params[:message_matches], index_column: :message_index) 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? return if from.blank? || from.is_gold?
if from.dmails.where("created_at > ?", 1.hour.ago).group(:to).reorder(nil).count.size >= 10 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
end end
@@ -182,10 +180,6 @@ class Dmail < ApplicationRecord
key ? "dmail ##{id}/#{self.key}" : "dmail ##{id}" key ? "dmail ##{id}/#{self.key}" : "dmail ##{id}"
end end
def self.searchable_includes
[:to, :from]
end
def self.available_includes def self.available_includes
[:owner, :to, :from] [:owner, :to, :from]
end end

View File

@@ -30,8 +30,7 @@ class DtextLink < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :link_type, :link_target, :model, :linked_wiki, :linked_tag)
q = q.search_attributes(params, :link_type, :link_target)
q.apply_default_order(params) q.apply_default_order(params)
end end
@@ -49,10 +48,6 @@ class DtextLink < ApplicationRecord
where(link_type: :wiki_link) where(link_type: :wiki_link)
end end
def self.searchable_includes
[:model, :linked_wiki, :linked_tag]
end
def self.available_includes def self.available_includes
[:model, :linked_wiki, :linked_tag] [:model, :linked_wiki, :linked_tag]
end end

View File

@@ -1,32 +1,67 @@
class EmailAddress < ApplicationRecord 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 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 :normalized_address, uniqueness: true
validates :user_id, uniqueness: true validates :user_id, uniqueness: true
validate :validate_deliverable, on: :deliverable validate :validate_deliverable, on: :deliverable
after_save :update_user 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) def address=(value)
self.normalized_address = EmailValidator.normalize(value) || address self.normalized_address = EmailValidator.normalize(value) || address
super super
end end
def nondisposable? def is_restricted?
EmailValidator.nondisposable?(normalized_address) 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 end
def validate_deliverable def validate_deliverable
if EmailValidator.undeliverable?(address) if EmailValidator.undeliverable?(address)
errors[:address] << "is invalid or does not exist" errors.add(:address, "is invalid or does not exist")
end end
end end
def update_user def update_user
user.update!(is_verified: is_verified? && nondisposable?) user.update!(is_verified: is_verified? && !is_restricted?)
end end
concerning :VerificationMethods do concerning :VerificationMethods do

View File

@@ -12,8 +12,7 @@ class Favorite < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :post)
q = q.search_attributes(params, :post)
if params[:user_id].present? if params[:user_id].present?
q = q.for_user(params[:user_id]) q = q.for_user(params[:user_id])

View File

@@ -26,8 +26,7 @@ class FavoriteGroup < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :name, :is_public, :post_ids, :creator)
q = q.search_attributes(params, :name, :is_public, :post_ids)
if params[:name_matches].present? if params[:name_matches].present?
q = q.name_matches(params[:name_matches]) q = q.name_matches(params[:name_matches])
@@ -56,13 +55,13 @@ class FavoriteGroup < ApplicationRecord
if !creator.is_platinum? if !creator.is_platinum?
error += " Upgrade your account to create more." error += " Upgrade your account to create more."
end end
self.errors.add(:base, error) errors.add(:base, error)
end end
end end
def validate_number_of_posts def validate_number_of_posts
if post_count > 10_000 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
end end
@@ -72,12 +71,12 @@ class FavoriteGroup < ApplicationRecord
nonexisting_post_ids = added_post_ids - existing_post_ids nonexisting_post_ids = added_post_ids - existing_post_ids
if nonexisting_post_ids.present? 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 end
duplicate_post_ids = post_ids.group_by(&:itself).transform_values(&:size).select { |id, count| count > 1 }.keys duplicate_post_ids = post_ids.group_by(&:itself).transform_values(&:size).select { |id, count| count > 1 }.keys
if duplicate_post_ids.present? 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
end end
@@ -164,10 +163,6 @@ class FavoriteGroup < ApplicationRecord
post_ids.include?(post_id) post_ids.include?(post_id)
end end
def self.searchable_includes
[:creator]
end
def self.available_includes def self.available_includes
[:creator] [:creator]
end end

View File

@@ -28,7 +28,7 @@ class ForumPost < ApplicationRecord
mentionable( mentionable(
:message_field => :body, :message_field => :body,
:title => ->(user_name) {%{#{creator.name} mentioned you in topic ##{topic_id} (#{topic.title})}}, :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 module SearchMethods
@@ -36,13 +36,16 @@ class ForumPost < ApplicationRecord
where(topic_id: ForumTopic.visible(user)) where(topic_id: ForumTopic.visible(user))
end 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) def search(params)
q = super 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.search_attributes(params, :is_deleted, :body)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index) q = q.text_attribute_matches(:body, params[:body_matches], index_column: :text_index)
if params[:linked_to].present? 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 end
q.apply_default_order(params) q.apply_default_order(params)
@@ -160,10 +163,6 @@ class ForumPost < ApplicationRecord
"forum ##{id}" "forum ##{id}"
end end
def self.searchable_includes
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
end
def self.available_includes def self.available_includes
[:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request] [:creator, :updater, :topic, :dtext_links, :votes, :tag_alias, :tag_implication, :bulk_update_request]
end end

View File

@@ -19,8 +19,7 @@ class ForumPostVote < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :score, :creator, :forum_post)
q = q.search_attributes(params, :score)
q = q.forum_post_matches(params[:forum_post]) q = q.forum_post_matches(params[:forum_post])
q.apply_default_order(params) q.apply_default_order(params)
end end
@@ -59,10 +58,6 @@ class ForumPostVote < ApplicationRecord
end end
end end
def self.searchable_includes
[:creator, :forum_post]
end
def self.available_includes def self.available_includes
[:creator, :forum_post] [:creator, :forum_post]
end end

View File

@@ -86,8 +86,7 @@ class ForumTopic < ApplicationRecord
end end
def search(params) def search(params)
q = super 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.search_attributes(params, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count)
q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index) q = q.text_attribute_matches(:title, params[:title_matches], index_column: :text_index)
if params[:is_private].to_s.truthy? if params[:is_private].to_s.truthy?
@@ -190,10 +189,6 @@ class ForumTopic < ApplicationRecord
title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "") title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "")
end end
def self.searchable_includes
[:creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications]
end
def self.available_includes def self.available_includes
[:creator, :updater, :original_post] [:creator, :updater, :original_post]
end end

View File

@@ -7,8 +7,7 @@ class ForumTopicVisit < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :user, :forum_topic_id, :last_read_at)
q = q.search_attributes(params, :user, :forum_topic_id, :last_read_at)
q.apply_default_order(params) q.apply_default_order(params)
end end
end end

View File

@@ -12,8 +12,7 @@ class IpAddress < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :ip_addr, :user, :model)
q = q.search_attributes(params, :ip_addr)
q.order(created_at: :desc) q.order(created_at: :desc)
end end
@@ -50,10 +49,6 @@ class IpAddress < ApplicationRecord
true true
end end
def self.searchable_includes
[:user, :model]
end
def self.available_includes def self.available_includes
[:user, :model] [:user, :model]
end end

View File

@@ -25,15 +25,18 @@ class IpBan < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :ip_addr, :reason, :is_deleted, :category, :hit_count, :last_hit_at, :creator)
q = q.search_attributes(params, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches]) q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:ip_addr].present? case params[:order]
q = q.where("ip_addr = ?", params[:ip_addr]) 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 end
q.apply_default_order(params) q
end end
def create_mod_action def create_mod_action
@@ -48,19 +51,19 @@ class IpBan < ApplicationRecord
def validate_ip_addr def validate_ip_addr
if ip_addr.blank? 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? 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 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 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 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 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? 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
end end
@@ -78,10 +81,6 @@ class IpBan < ApplicationRecord
super(ip_addr.strip) super(ip_addr.strip)
end end
def self.searchable_includes
[:creator]
end
def self.available_includes def self.available_includes
[:creator] [:creator]
end end

View File

@@ -61,9 +61,7 @@ class ModAction < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :category, :description, :creator)
q = q.search_attributes(params, :category, :description)
q = q.text_attribute_matches(:description, params[:description_matches]) q = q.text_attribute_matches(:description, params[:description_matches])
q.apply_default_order(params) q.apply_default_order(params)
@@ -77,10 +75,6 @@ class ModAction < ApplicationRecord
create(creator: user, description: desc, category: categories[cat]) create(creator: user, description: desc, category: categories[cat])
end end
def self.searchable_includes
[:creator]
end
def self.available_includes def self.available_includes
[:creator] [:creator]
end end

View File

@@ -82,17 +82,12 @@ class ModerationReport < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model)
q = q.search_attributes(params, :reason)
q = q.text_attribute_matches(:reason, params[:reason_matches]) q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params) q.apply_default_order(params)
end end
def self.searchable_includes
[:creator, :model]
end
def self.available_includes def self.available_includes
[:creator, :model] [:creator, :model]
end end

View File

@@ -14,9 +14,7 @@ class Note < ApplicationRecord
module SearchMethods module SearchMethods
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :post)
q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index) q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index)
q.apply_default_order(params) q.apply_default_order(params)
@@ -26,13 +24,13 @@ class Note < ApplicationRecord
extend SearchMethods extend SearchMethods
def validate_post_is_not_locked 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 end
def note_within_image def note_within_image
return false unless post.present? 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) 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
end end
@@ -129,10 +127,6 @@ class Note < ApplicationRecord
new_note.save new_note.save
end end
def self.searchable_includes
[:post]
end
def self.available_includes def self.available_includes
[:post] [:post]
end end

View File

@@ -4,9 +4,7 @@ class NoteVersion < ApplicationRecord
belongs_to_updater :counter_cache => "note_update_count" belongs_to_updater :counter_cache => "note_update_count"
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :updater, :note, :post)
q = q.search_attributes(params, :is_active, :x, :y, :width, :height, :body, :version)
q = q.text_attribute_matches(:body, params[:body_matches]) q = q.text_attribute_matches(:body, params[:body_matches])
q.apply_default_order(params) q.apply_default_order(params)
@@ -71,10 +69,6 @@ class NoteVersion < ApplicationRecord
end end
end end
def self.searchable_includes
[:updater, :note, :post]
end
def self.available_includes def self.available_includes
[:updater, :note, :post] [:updater, :note, :post]
end end

View File

@@ -4,17 +4,12 @@ class PixivUgoiraFrameData < ApplicationRecord
serialize :data serialize :data
before_validation :normalize_data, on: :create before_validation :normalize_data, on: :create
def self.searchable_includes
[:post]
end
def self.available_includes def self.available_includes
[:post] [:post]
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :data, :content_type, :post)
q = q.search_attributes(params, :data, :content_type)
q.apply_default_order(params) q.apply_default_order(params)
end end

View File

@@ -36,9 +36,7 @@ class Pool < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :is_deleted, :name, :description, :post_ids)
q = q.search_attributes(params, :is_deleted, :name, :description, :post_ids)
q = q.text_attribute_matches(:description, params[:description_matches]) q = q.text_attribute_matches(:description, params[:description_matches])
if params[:post_tags_match] if params[:post_tags_match]
@@ -147,7 +145,7 @@ class Pool < ApplicationRecord
def updater_can_edit_deleted def updater_can_edit_deleted
if is_deleted? && !Pundit.policy!([CurrentUser.user, nil], self).update? 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
end end
@@ -254,23 +252,23 @@ class Pool < ApplicationRecord
def validate_name def validate_name
case name case name
when /\A(any|none|series|collection)\z/i 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 /,/ when /,/
errors[:name] << "cannot contain commas" errors.add(:name, "cannot contain commas")
when /\*/ when /\*/
errors[:name] << "cannot contain asterisks" errors.add(:name, "cannot contain asterisks")
when /\A_/ when /\A_/
errors[:name] << "cannot begin with an underscore" errors.add(:name, "cannot begin with an underscore")
when /_\z/ when /_\z/
errors[:name] << "cannot end with an underscore" errors.add(:name, "cannot end with an underscore")
when /__/ when /__/
errors[:name] << "cannot contain consecutive underscores" errors.add(:name, "cannot contain consecutive underscores")
when /[^[:graph:]]/ when /[^[:graph:]]/
errors[:name] << "cannot contain non-printable characters" errors.add(:name, "cannot contain non-printable characters")
when "" when ""
errors[:name] << "cannot be blank" errors.add(:name, "cannot be blank")
when /\A[0-9]+\z/ when /\A[0-9]+\z/
errors[:name] << "cannot contain only digits" errors.add(:name, "cannot contain only digits")
end end
end end
end end

View File

@@ -32,8 +32,7 @@ class PoolVersion < ApplicationRecord
end end
def search(params) def search(params)
q = super 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)
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)
if params[:post_id] if params[:post_id]
q = q.for_post_id(params[:post_id].to_i) q = q.for_post_id(params[:post_id].to_i)

View File

@@ -20,6 +20,7 @@ class Post < ApplicationRecord
before_validation :remove_parent_loops before_validation :remove_parent_loops
validates_uniqueness_of :md5, :on => :create, message: ->(obj, data) { "duplicate: #{Post.find_by_md5(obj.md5).id}"} 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_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 :added_tags_are_valid
validate :removed_tags_are_valid validate :removed_tags_are_valid
validate :has_artist_tag validate :has_artist_tag
@@ -55,6 +56,7 @@ class Post < ApplicationRecord
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
has_many :favorites has_many :favorites
has_many :favorited_users, through: :favorites, source: :user
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy 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 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| invalid_tags.each do |tag|
tag.errors.messages.each do |attribute, messages| 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
end end
@@ -760,12 +762,11 @@ class Post < ApplicationRecord
update_column(:fav_count, fav_count) update_column(:fav_count, fav_count)
end end
def favorited_by?(user_id = CurrentUser.id) def favorited_by?(user)
fav_string.match?(/(?:\A| )fav:#{user_id}(?:\Z| )/) return false if user.is_anonymous?
Favorite.exists?(post: self, user: user)
end end
alias is_favorited? favorited_by?
def append_user_to_fav_string(user_id) def append_user_to_fav_string(user_id)
update_column(:fav_string, (fav_string + " fav:#{user_id}").strip) update_column(:fav_string, (fav_string + " fav:#{user_id}").strip)
clean_fav_string! if clean_fav_string? clean_fav_string! if clean_fav_string?
@@ -801,14 +802,11 @@ class Post < ApplicationRecord
false false
end end
# users who favorited this post, ordered by users who favorited it first # Users who publicly favorited this post, ordered by time of favorite.
def favorited_users def visible_favorited_users(viewer)
favorited_user_ids = fav_string.scan(/\d+/).map(&:to_i) favorited_users.order("favorites.id DESC").select do |fav_user|
visible_users = User.find(favorited_user_ids).select do |user| Pundit.policy!([viewer, nil], fav_user).can_see_favorites?
Pundit.policy!([CurrentUser.user, nil], user).can_see_favorites?
end end
ordered_users = visible_users.index_by(&:id).slice(*favorited_user_ids).values
ordered_users
end end
def favorite_groups def favorite_groups
@@ -976,7 +974,7 @@ class Post < ApplicationRecord
module DeletionMethods module DeletionMethods
def expunge! def expunge!
if is_status_locked? if is_status_locked?
self.errors.add(:is_status_locked, "; cannot delete post") errors.add(:is_status_locked, "; cannot delete post")
return false return false
end end
@@ -1082,11 +1080,11 @@ class Post < ApplicationRecord
def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS) def copy_notes_to(other_post, copy_tags: NOTE_COPY_TAGS)
transaction do transaction do
if id == other_post.id 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 return false
end end
unless has_notes? unless has_notes?
errors.add :post, "has no notes" errors.add(:post, "has no notes")
return false return false
end end
@@ -1289,14 +1287,17 @@ class Post < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(
q = q.search_attributes(
params, params,
:rating, :source, :pixiv_id, :fav_count, :score, :up_score, :down_score, :md5, :file_ext, :id, :created_at, :updated_at, :rating, :source, :pixiv_id, :fav_count,
:file_size, :image_width, :image_height, :tag_count, :has_children, :has_active_children, :score, :up_score, :down_score, :md5, :file_ext, :file_size, :image_width,
:is_note_locked, :is_rating_locked, :is_status_locked, :is_pending, :is_flagged, :is_deleted, :image_height, :tag_count, :has_children, :has_active_children,
:is_banned, :last_comment_bumped_at, :last_commented_at, :last_noted_at :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? if params[:tags].present?
@@ -1357,8 +1358,7 @@ class Post < ApplicationRecord
module ValidationMethods module ValidationMethods
def post_is_not_its_own_parent def post_is_not_its_own_parent
if !new_record? && id == parent_id if !new_record? && id == parent_id
errors[:base] << "Post cannot have itself as a parent" errors.add(:base, "Post cannot have itself as a parent")
false
end end
end end
@@ -1372,30 +1372,23 @@ class Post < ApplicationRecord
end end
def uploader_is_not_limited 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 end
def added_tags_are_valid def added_tags_are_valid
new_tags = added_tags.select(&:empty?) new_tags = added_tags.select(&:empty?)
new_general_tags = new_tags.select(&:general?) new_artist_tags, new_general_tags = new_tags.partition(&:artist?)
new_artist_tags = new_tags.select(&:artist?)
repopulated_tags = new_tags.select { |t| !t.general? && !t.meta? && (t.created_at < 1.hour.ago) }
if new_general_tags.present? if new_general_tags.present?
n = new_general_tags.size n = new_general_tags.size
tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" } tag_wiki_links = new_general_tags.map { |tag| "[[#{tag.name}]]" }
self.warnings[:base] << "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}" warnings.add(:base, "Created #{n} new #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}")
end
if repopulated_tags.present?
n = repopulated_tags.size
tag_wiki_links = repopulated_tags.map { |tag| "[[#{tag.name}]]" }
self.warnings[:base] << "Repopulated #{n} old #{(n == 1) ? "tag" : "tags"}: #{tag_wiki_links.join(", ")}"
end end
new_artist_tags.each do |tag| new_artist_tags.each do |tag|
if tag.artist.blank? 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 end
end end
@@ -1406,7 +1399,7 @@ class Post < ApplicationRecord
if unremoved_tags.present? if unremoved_tags.present?
unremoved_tags_list = unremoved_tags.map { |t| "[[#{t}]]" }.to_sentence 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
end end
@@ -1417,21 +1410,22 @@ class Post < ApplicationRecord
return if tags.any?(&:artist?) return if tags.any?(&:artist?)
return if Sources::Strategies.find(source).is_a?(Sources::Strategies::Null) 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 end
def has_copyright_tag def has_copyright_tag
return if !new_record? return if !new_record?
return if has_tag?("copyright_request") || tags.any?(&:copyright?) 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 end
def has_enough_tags def has_enough_tags
return if !new_record? return if !new_record?
if tags.count(&:general?) < 10 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 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)) super.where(table[:is_pending].eq(false)).where(table[:is_flagged].eq(false)).where(table[:is_deleted].eq(false))
end 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 def self.available_includes
[:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data] [:uploader, :updater, :approver, :parent, :upload, :artist_commentary, :flags, :appeals, :notes, :comments, :children, :approvals, :replacements, :pixiv_ugoira_frame_data]
end end

View File

@@ -17,8 +17,7 @@ class PostAppeal < ApplicationRecord
module SearchMethods module SearchMethods
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :creator, :post)
q = q.search_attributes(params, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches]) q = q.text_attribute_matches(:reason, params[:reason_matches])
q.apply_default_order(params) q.apply_default_order(params)
@@ -28,15 +27,11 @@ class PostAppeal < ApplicationRecord
extend SearchMethods extend SearchMethods
def validate_creator_is_not_limited 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 end
def validate_post_is_appealable def validate_post_is_appealable
errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable? errors.add(:post, "cannot be appealed") if post.is_status_locked? || !post.is_appealable?
end
def self.searchable_includes
[:creator, :post]
end end
def self.available_includes def self.available_includes

View File

@@ -38,14 +38,10 @@ class PostApproval < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :user, :post)
q.apply_default_order(params) q.apply_default_order(params)
end end
def self.searchable_includes
[:user, :post]
end
def self.available_includes def self.available_includes
[:user, :post] [:user, :post]
end end

View File

@@ -21,9 +21,7 @@ class PostDisapproval < ApplicationRecord
concerning :SearchMethods do concerning :SearchMethods do
class_methods do class_methods do
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :message, :reason, :user, :post)
q = q.search_attributes(params, :message, :reason)
q = q.text_attribute_matches(:message, params[:message_matches]) q = q.text_attribute_matches(:message, params[:message_matches])
q = q.with_message if params[:has_message].to_s.truthy? q = q.with_message if params[:has_message].to_s.truthy?
@@ -41,17 +39,13 @@ class PostDisapproval < ApplicationRecord
end end
end end
def self.searchable_includes
[:user, :post]
end
def self.available_includes def self.available_includes
[:user, :post] [:user, :post]
end end
def validate_disapproval def validate_disapproval
if post.is_active? 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
end end

View File

@@ -56,9 +56,7 @@ class PostFlag < ApplicationRecord
end end
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :reason, :status, :post)
q = q.search_attributes(params, :reason, :status)
q = q.text_attribute_matches(:reason, params[:reason_matches]) q = q.text_attribute_matches(:reason, params[:reason_matches])
if params[:creator_id].present? if params[:creator_id].present?
@@ -95,17 +93,17 @@ class PostFlag < ApplicationRecord
end end
def validate_creator_is_not_limited 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 end
def validate_post def validate_post
errors[:post] << "is pending and cannot be flagged" if post.is_pending? && !is_deletion errors.add(: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.add(: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 locked and cannot be flagged") if post.is_status_locked?
flag = post.flags.in_cooldown.last flag = post.flags.in_cooldown.last
if !is_deletion && flag.present? 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
end end
@@ -113,10 +111,6 @@ class PostFlag < ApplicationRecord
post.uploader_id post.uploader_id
end end
def self.searchable_includes
[:post]
end
def self.available_includes def self.available_includes
[:post] [:post]
end end

View File

@@ -11,18 +11,17 @@ class PostReplacement < ApplicationRecord
self.original_url = post.source self.original_url = post.source
self.tags = post.tag_string + " " + self.tags.to_s self.tags = post.tag_string + " " + self.tags.to_s
self.file_ext_was = post.file_ext self.old_file_ext = post.file_ext
self.file_size_was = post.file_size self.old_file_size = post.file_size
self.image_width_was = post.image_width self.old_image_width = post.image_width
self.image_height_was = post.image_height self.old_image_height = post.image_height
self.md5_was = post.md5 self.old_md5 = post.md5
end end
concerning :Search do concerning :Search do
class_methods do class_methods do
def search(params = {}) def search(params = {})
q = super q = search_attributes(params, :id, :created_at, :updated_at, :md5, :old_md5, :file_ext, :old_file_ext, :original_url, :replacement_url, :creator, :post)
q = q.search_attributes(params, :md5, :md5_was, :file_ext, :file_ext_was, :original_url, :replacement_url)
q.apply_default_order(params) q.apply_default_order(params)
end end
end end
@@ -39,10 +38,6 @@ class PostReplacement < ApplicationRecord
tags.join(" ") tags.join(" ")
end end
def self.searchable_includes
[:creator, :post]
end
def self.available_includes def self.available_includes
[:creator, :post] [:creator, :post]
end end

View File

@@ -38,8 +38,7 @@ class PostVersion < ApplicationRecord
end end
def search(params) def search(params)
q = super 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)
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)
if params[:changed_tags] if params[:changed_tags]
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/)) q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))

View File

@@ -19,8 +19,7 @@ class PostVote < ApplicationRecord
end end
def self.search(params) def self.search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post)
q = q.search_attributes(params, :score)
q.apply_default_order(params) q.apply_default_order(params)
end end
@@ -50,10 +49,6 @@ class PostVote < ApplicationRecord
end end
end end
def self.searchable_includes
[:user, :post]
end
def self.available_includes def self.available_includes
[:user, :post] [:user, :post]
end end

View File

@@ -63,17 +63,12 @@ class SavedSearch < ApplicationRecord
.gsub(/[[:space:]]/, "_") .gsub(/[[:space:]]/, "_")
end end
def search_labels(user_id, params) def all_labels
labels = labels_for(user_id) select(Arel.sql("distinct unnest(labels) as label")).order(:label)
end
if params[:label].present? def labels_like(label)
query = Regexp.escape(params[:label]).gsub("\\*", ".*") all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
query = ".*#{query}.*" unless query.include?("*")
query = /\A#{query}\z/
labels = labels.grep(query)
end
labels
end end
def labels_for(user_id) def labels_for(user_id)
@@ -105,8 +100,7 @@ class SavedSearch < ApplicationRecord
concerning :Search do concerning :Search do
class_methods do class_methods do
def search(params) def search(params)
q = super q = search_attributes(params, :id, :created_at, :updated_at, :query)
q = q.search_attributes(params, :query)
if params[:label] if params[:label]
q = q.labeled(params[:label]) q = q.labeled(params[:label])
@@ -174,7 +168,7 @@ class SavedSearch < ApplicationRecord
def validate_count def validate_count
if user.saved_searches.count >= user.max_saved_searches 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
end end

View File

@@ -1,4 +1,6 @@
class Tag < ApplicationRecord 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 :wiki_page, :foreign_key => "title", :primary_key => "name"
has_one :artist, :foreign_key => "name", :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" 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 Tag.where(name: tag_name).pick(:category).to_i
end 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 = {}) def categories_for(tag_names, options = {})
if options[:disable_caching] if options[:disable_caching]
Array(tag_names).inject({}) do |hash, tag_name| Array(tag_names).inject({}) do |hash, tag_name|
@@ -232,16 +222,19 @@ class Tag < ApplicationRecord
end end
module SearchMethods 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 # ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
def order_similarity(name) 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. order(Arel.sql("levenshtein(left(name, 255), #{connection.quote(name)}), tags.post_count DESC, tags.name ASC"))
# 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")
end end
# ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336 # ref: https://www.postgresql.org/docs/current/static/pgtrgm.html#idm46428634524336
def fuzzy_name_matches(name) 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 end
def name_matches(name) def name_matches(name)
@@ -249,21 +242,29 @@ class Tag < ApplicationRecord
end end
def alias_matches(name) 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 end
def name_or_alias_matches(name) def name_or_alias_matches(name)
name_matches(name).or(alias_matches(name)) name_matches(name).or(alias_matches(name))
end end
def wildcard_matches(tag, limit: 25) def wildcard_matches(tag)
nonempty.name_matches(tag).order(post_count: :desc, name: :asc).limit(limit).pluck(:name) 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 end
def search(params) def search(params)
q = super 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)
q = q.search_attributes(params, :is_locked, :category, :post_count, :name)
if params[:fuzzy_name_matches].present? if params[:fuzzy_name_matches].present?
q = q.fuzzy_name_matches(params[:fuzzy_name_matches]) q = q.fuzzy_name_matches(params[:fuzzy_name_matches])
@@ -352,12 +353,20 @@ class Tag < ApplicationRecord
Post.system_tag_match(name) Post.system_tag_match(name)
end end
def self.model_restriction(table) def abbreviation
super.where(table[:post_count].gt(0)) name.gsub(ABBREVIATION_REGEXP, "\\1")
end end
def self.searchable_includes def tag_alias_for_pattern(pattern)
[:wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links] 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 end
def self.available_includes def self.available_includes

View File

@@ -7,7 +7,15 @@ class TagAlias < TagRelationship
def self.to_aliased(names) def self.to_aliased(names)
names = Array(names).map(&:to_s) names = Array(names).map(&:to_s)
return [] if names.empty? return [] if names.empty?
aliases = active.where(antecedent_name: names).map { |ta| [ta.antecedent_name, ta.consequent_name] }.to_h 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 } names.map { |name| aliases[name] || name }
end end
@@ -23,7 +31,7 @@ class TagAlias < TagRelationship
tag_alias = TagAlias.active.find_by(antecedent_name: consequent_name) tag_alias = TagAlias.active.find_by(antecedent_name: consequent_name)
if tag_alias.present? && tag_alias.consequent_name != antecedent_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
end end

Some files were not shown because too many files have changed in this diff Show More