diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 000000000..b64ad2111 --- /dev/null +++ b/.bundle/config @@ -0,0 +1,3 @@ +--- +BUNDLE_BUILD__NOKOGIRI: "--use-system-libraries" +BUNDLE_BUILD__NOKOGUMBO: "--without-libxml2" diff --git a/.codeclimate.yml b/.codeclimate.yml index dbb000b8a..646bfcd16 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,17 +1,25 @@ version: 2 checks: argument-count: + enabled: false + complex-logic: config: - threshold: 4 + threshold: 8 file-lines: config: - threshold: 500 + threshold: 1000 + method-complexity: + config: + threshold: 15 method-count: - config: - threshold: 40 + enabled: false method-lines: + enabled: false + nested-control-flow: config: - threshold: 100 + threshold: 4 + return-statements: + enabled: false plugins: eslint: enabled: true diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2f5475e44 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[**.{js,rb,css,erb,md,json,yml}] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[**.html.erb] +indent_size = unset + +[app/javascript/vendor/**] +indent_size = unset diff --git a/.eslintrc.yml b/.eslintrc.yml index 1bb08c0bc..df6581f1c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -8,9 +8,13 @@ parserOptions: globals: $: false require: false +parser: babel-eslint +plugins: + - babel rules: # https://eslint.org/docs/rules/ array-callback-return: error + babel/no-unused-expressions: error block-scoped-var: error consistent-return: error default-case: error @@ -32,7 +36,7 @@ rules: no-sequences: error no-shadow: error no-shadow-restricted-names: error - no-unused-expressions: error + #no-unused-expressions: error no-unused-vars: - error - argsIgnorePattern: "^_" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 52d1c81c9..4dd39cc50 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,20 +1,40 @@ -name: Test +name: Github -on: [push, pull_request] +# Trigger on pushes to master or pull requests to master, but not both. +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: + # https://github.com/Marr11317/ConflictAdviser + notify-merge-conflicts: + runs-on: ubuntu-latest + steps: + - uses: Marr11317/ConflictAdviser@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + conflict_label: "merge conflict" + comment: 'Rebase needed' + test: runs-on: ubuntu-latest container: ubuntu:20.04 + defaults: + run: + shell: bash + env: DEBIAN_FRONTEND: noninteractive PARALLEL_WORKERS: 8 # number of parallel tests to run + RUBYOPT: -W0 # silence ruby warnings + VIPS_WARNING: 0 # silence libvips warnings - # Code Climate configuration. https://docs.codeclimate.com/docs/finding-your-test-coverage-token - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - GIT_COMMIT_SHA: ${{ github.sha }} - GIT_BRANCH: ${{ github.ref }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru ARCHIVE_DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru @@ -45,32 +65,25 @@ jobs: POSTGRES_PASSWORD: danbooru steps: - - name: Check out code - uses: actions/checkout@v2 -# - name: Save environment -# run: env | egrep "DANBOORU|DATABASE_URL" > ~/.env -# - name: Install docker-compose -# run: sudo apt-get update && sudo apt-get -y install docker-compose -# - name: Run tests -# run: docker-compose --env-file ~/.env -f config/docker/docker-compose.test.yaml -p danbooru up - name: Install OS dependencies run: | apt-get update - apt-get -y install --no-install-recommends build-essential ruby ruby-dev ruby-bundler git nodejs yarnpkg webpack ffmpeg mkvtoolnix libvips-dev libxml2-dev postgresql-server-dev-all wget + apt-get -y install --no-install-recommends build-essential ruby ruby-dev ruby-bundler git nodejs yarnpkg webpack ffmpeg mkvtoolnix libvips-dev libxml2-dev libxslt-dev zlib1g-dev postgresql-server-dev-all wget curl git ln -sf /usr/bin/yarnpkg /usr/bin/yarn + + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install Ruby dependencies run: BUNDLE_DEPLOYMENT=true bundle install --jobs 4 + - name: Install Javascript dependencies run: yarn install + - name: Prepare database run: config/docker/prepare-tests.sh - # https://docs.codeclimate.com/docs/configuring-test-coverage - - name: Prepare test coverage for Code Climate - run: | - wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 - chmod +x test-reporter-latest-linux-amd64 - ./test-reporter-latest-linux-amd64 before-build + - name: Run tests run: bin/rails test - - name: Upload test coverage to Code Climate - run: ./test-reporter-latest-linux-amd64 after-build diff --git a/.gitignore b/.gitignore index 8f4292733..a51a3e83b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .yarn-integrity .gitconfig .git/ -.bundle/ config/database.yml config/danbooru_local_config.rb config/deploy/*.rb diff --git a/.rubocop.yml b/.rubocop.yml index 001265032..371b1967c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,8 +1,9 @@ +# some of these settings are overriden in also test/.rubocop.yml + require: - rubocop-rails AllCops: - TargetRubyVersion: 2.7.0 NewCops: enable Exclude: - "bin/*" @@ -41,14 +42,22 @@ Layout/SpaceInsideHashLiteralBraces: Metrics/AbcSize: Enabled: false +Metrics/BlockLength: + Max: 50 + ExcludedMethods: + - concerning + - context + - should + Metrics/BlockNesting: + CountBlocks: false Max: 4 Metrics/ClassLength: Max: 500 Metrics/CyclomaticComplexity: - Max: 10 + Enabled: false Metrics/MethodLength: Max: 100 @@ -60,7 +69,7 @@ Metrics/ParameterLists: Max: 4 Metrics/PerceivedComplexity: - Enabled: false + Max: 20 Lint/InheritException: EnforcedStyle: standard_error @@ -110,7 +119,9 @@ Style/NumericPredicate: Style/PercentLiteralDelimiters: PreferredDelimiters: "default": "[]" - "%r": "!!" + +Style/ParallelAssignment: + Enabled: false Style/PerlBackrefs: Enabled: false diff --git a/.simplecov b/.simplecov index 628307e3c..e527eadf6 100644 --- a/.simplecov +++ b/.simplecov @@ -1,8 +1,12 @@ SimpleCov.start "rails" do add_group "Libraries", ["app/logical", "lib"] add_group "Presenters", "app/presenters" - #enable_coverage :branch - #minimum_coverage line: 85, branch: 75 - #minimum_coverage_by_file 50 - #coverage_dir "tmp/coverage" + add_group "Policies", "app/policies" + enable_coverage :branch + + # https://github.com/codecov/codecov-ruby#submit-only-in-ci-example + if ENV["CODECOV_TOKEN"] + require "codecov" + SimpleCov.formatter = SimpleCov::Formatter::Codecov + end end diff --git a/Gemfile b/Gemfile index ad30bc238..9f109c6ae 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem "pg" gem "delayed_job" gem "delayed_job_active_record" gem "simple_form" -gem "mechanize" gem "whenever", :require => false gem "sanitize" gem 'ruby-vips' @@ -28,14 +27,11 @@ gem 'daemons' gem 'oauth2' gem 'bootsnap' gem 'addressable' -gem 'httparty' gem 'rakismet' gem 'recaptcha', require: "recaptcha/rails" gem 'activemodel-serializers-xml' -gem 'jquery-rails' gem 'webpacker', '>= 4.0.x' gem 'rake' -gem 'retriable' gem 'redis' gem 'request_store' gem 'builder' @@ -47,9 +43,7 @@ gem 'http' gem 'activerecord-hierarchical_query' gem 'pundit' gem 'mail' - -# locked to 1.10.9 to workaround an incompatibility with nokogumbo 2.0.2. -gem 'nokogiri', '~> 1.10.9' +gem 'nokogiri' group :production, :staging do gem 'unicorn', :platforms => :ruby @@ -65,7 +59,6 @@ end group :development do gem 'rubocop' gem 'rubocop-rails' - gem 'sinatra' gem 'meta_request' gem 'rack-mini-profiler' gem 'stackprof' @@ -85,11 +78,11 @@ group :test do gem "factory_bot" gem "mocha", require: "mocha/minitest" gem "ffaker" - gem "simplecov", "~> 0.17.0", require: false - gem "webmock", require: "webmock/minitest" + gem "simplecov", require: false gem "minitest-ci" gem "minitest-reporters", require: "minitest/reporters" gem "mock_redis" gem "capybara" gem "selenium-webdriver" + gem "codecov", require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 9d09ede27..fdb68bc26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,70 +1,70 @@ GIT remote: https://github.com/evazion/dtext_rb.git - revision: 507e97e0963822c20351c82620c28cc8e23423d5 + revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7 specs: - dtext_rb (1.10.5) + dtext_rb (1.10.6) nokogiri (~> 1.8) GEM remote: https://rubygems.org/ specs: - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) + actioncable (6.0.3.2) + actionpack (= 6.0.3.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + actionmailbox (6.0.3.2) + actionpack (= 6.0.3.2) + activejob (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) + actionmailer (6.0.3.2) + actionpack (= 6.0.3.2) + actionview (= 6.0.3.2) + activejob (= 6.0.3.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) + actionpack (6.0.3.2) + actionview (= 6.0.3.2) + activesupport (= 6.0.3.2) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + actiontext (6.0.3.2) + actionpack (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) + actionview (6.0.3.2) + activesupport (= 6.0.3.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) + activejob (6.0.3.2) + activesupport (= 6.0.3.2) globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) + activemodel (6.0.3.2) + activesupport (= 6.0.3.2) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) + activerecord (6.0.3.2) + activemodel (= 6.0.3.2) + activesupport (= 6.0.3.2) activerecord-hierarchical_query (1.2.3) activerecord (>= 5.0, < 6.1) pg (>= 0.21, < 1.3) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) + activestorage (6.0.3.2) + actionpack (= 6.0.3.2) + activejob (= 6.0.3.2) + activerecord (= 6.0.3.2) marcel (~> 0.3.1) - activesupport (6.0.3.1) + activesupport (6.0.3.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -75,42 +75,42 @@ GEM airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) ansi (1.5.0) - ast (2.4.0) + ast (2.4.1) aws-eventstream (1.1.0) - aws-partitions (1.326.0) - aws-sdk-core (3.98.0) + aws-partitions (1.341.0) + aws-sdk-core (3.103.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-sqs (1.26.0) - aws-sdk-core (~> 3, >= 3.71.0) + aws-sdk-sqs (1.30.0) + aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.4) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sigv4 (1.2.1) + aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.13) bootsnap (1.4.6) msgpack (~> 1.0) builder (3.2.4) byebug (11.1.3) - capistrano (3.14.0) + capistrano (3.14.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.6.0) + capistrano-bundler (2.0.0) capistrano (~> 3.1) capistrano-deploytags (1.0.7) capistrano (>= 3.7.0) - capistrano-rails (1.5.0) + capistrano-rails (1.6.1) capistrano (~> 3.1) - capistrano-bundler (~> 1.1) - capistrano-rbenv (2.1.6) + capistrano-bundler (>= 1.1, < 3) + capistrano-rbenv (2.2.0) capistrano (~> 3.1) sshkit (~> 1.3) capistrano3-unicorn (0.2.1) capistrano (~> 3.1, >= 3.1.0) - capybara (3.32.2) + capybara (3.33.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -120,11 +120,13 @@ GEM xpath (~> 3.2) childprocess (3.0.0) chronic (0.10.2) + codecov (0.2.0) + colorize + json + simplecov coderay (1.1.3) + colorize (0.8.1) concurrent-ruby (1.1.6) - connection_pool (2.2.3) - crack (0.4.3) - safe_yaml (~> 1.0.0) crass (1.0.6) daemons (1.3.1) delayed_job (4.1.8) @@ -132,22 +134,21 @@ GEM delayed_job_active_record (4.1.4) activerecord (>= 3.0, < 6.1) delayed_job (>= 3.0, < 5) - diff-lcs (1.3) + diff-lcs (1.4.4) docile (1.3.2) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.5) - dotenv-rails (2.7.5) - dotenv (= 2.7.5) - railties (>= 3.2, < 6.1) + dotenv (2.7.6) + dotenv-rails (2.7.6) + dotenv (= 2.7.6) + railties (>= 3.2) erubi (1.9.0) - factory_bot (5.2.0) - activesupport (>= 4.2.0) + factory_bot (6.1.0) + activesupport (>= 5.0.0) faraday (1.0.1) multipart-post (>= 1.2, < 3) ffaker (2.15.0) - ffi (1.13.0) - ffi (1.13.0-x64-mingw32) + ffi (1.13.1) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake @@ -156,7 +157,6 @@ GEM ffi (~> 1.0) globalid (0.4.2) activesupport (>= 4.2.0) - hashdiff (1.0.1) http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) @@ -167,48 +167,29 @@ GEM http-form_data (2.3.0) http-parser (1.2.1) ffi-compiler (>= 1.0, < 2.0) - httparty (0.18.0) - mime-types (~> 3.0) - multi_xml (>= 0.5.2) i18n (1.8.3) concurrent-ruby (~> 1.0) ipaddress_2 (0.13.0) jmespath (1.4.0) - jquery-rails (4.3.5) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) - json (2.3.0) + json (2.3.1) jwt (2.2.1) kgio (2.11.3) listen (3.2.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.5.0) + loofah (2.6.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) marcel (0.3.3) mimemagic (~> 0.3.2) - mechanize (2.7.6) - domain_name (~> 0.5, >= 0.5.1) - http-cookie (~> 1.0) - mime-types (>= 1.17.2) - net-http-digest_auth (~> 1.1, >= 1.1.1) - net-http-persistent (>= 2.5.2) - nokogiri (~> 1.6) - ntlm-http (~> 0.1, >= 0.1.1) - webrobots (>= 0.0.9, < 0.2) memoist (0.16.2) memory_profiler (0.9.14) meta_request (0.7.2) rack-contrib (>= 1.1, < 3) railties (>= 3.0.0, < 7) method_source (1.0.0) - mime-types (3.3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2020.0512) mimemagic (0.3.5) mini_mime (1.0.2) mini_portile2 (2.4.0) @@ -221,42 +202,32 @@ GEM minitest (>= 5.0) ruby-progressbar mocha (1.11.2) - mock_redis (0.23.0) + mock_redis (0.25.0) msgpack (1.3.3) - msgpack (1.3.3-x64-mingw32) - multi_json (1.14.1) + multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) - mustermann (1.1.1) - ruby2_keywords (~> 0.0.1) - net-http-digest_auth (1.4.1) - net-http-persistent (4.0.0) - connection_pool (~> 2.2) net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-sftp (3.0.0) net-ssh (>= 5.0.0, < 7.0.0) - net-ssh (6.0.2) + net-ssh (6.1.0) newrelic_rpm (6.11.0.365) nio4r (2.5.2) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) - nokogiri (1.10.9-x64-mingw32) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) nokogiri (~> 1.8, >= 1.8.4) - ntlm-http (0.1.1) oauth2 (1.4.4) faraday (>= 0.8, < 2.0) jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - parallel (1.19.1) - parser (2.7.1.3) - ast (~> 2.4.0) + parallel (1.19.2) + parser (2.7.1.4) + ast (~> 2.4.1) pg (1.2.3) - pg (1.2.3-x64-mingw32) pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) @@ -270,40 +241,38 @@ GEM nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) - rack (2.2.2) + rack (2.2.3) rack-contrib (2.2.0) rack (~> 2.0) rack-mini-profiler (2.0.2) rack (>= 1.2.0) - rack-protection (2.0.8.1) - rack rack-proxy (0.6.5) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + rails (6.0.3.2) + actioncable (= 6.0.3.2) + actionmailbox (= 6.0.3.2) + actionmailer (= 6.0.3.2) + actionpack (= 6.0.3.2) + actiontext (= 6.0.3.2) + actionview (= 6.0.3.2) + activejob (= 6.0.3.2) + activemodel (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) bundler (>= 1.3.0) - railties (= 6.0.3.1) + railties (= 6.0.3.2) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) + railties (6.0.3.2) + actionpack (= 6.0.3.2) + activesupport (= 6.0.3.2) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -316,25 +285,24 @@ GEM ffi (~> 1.0) recaptcha (5.5.0) json - redis (4.1.4) + redis (4.2.1) regexp_parser (1.7.1) request_store (1.5.0) rack (>= 1.4) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - retriable (3.1.2) rexml (3.2.4) - rubocop (0.85.1) + rubocop (0.88.0) parallel (~> 1.10) - parser (>= 2.7.0.1) + parser (>= 2.7.1.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.7) rexml - rubocop-ast (>= 0.0.3) + rubocop-ast (>= 0.1.0, < 1.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.0.3) + rubocop-ast (0.1.0) parser (>= 2.7.0.1) rubocop-rails (2.6.0) activesupport (>= 4.2.0) @@ -343,10 +311,8 @@ GEM ruby-progressbar (1.10.1) ruby-vips (2.0.17) ffi (~> 1.9) - ruby2_keywords (0.0.2) rubyzip (2.3.0) - safe_yaml (1.0.5) - sanitize (5.2.0) + sanitize (5.2.1) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) @@ -357,23 +323,17 @@ GEM childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) semantic_range (2.3.0) - shoulda-context (1.2.2) + shoulda-context (2.0.0) shoulda-matchers (4.3.0) activesupport (>= 4.2.0) simple_form (5.0.2) actionpack (>= 5.0) activemodel (>= 5.0) - simplecov (0.17.1) + simplecov (0.18.5) docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) - sinatra (2.0.8.1) - mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.8.1) - tilt (~> 2.0) - sprockets (4.0.0) + simplecov-html (~> 0.11) + simplecov-html (0.12.2) + sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) @@ -389,13 +349,11 @@ GEM stripe (5.22.0) thor (1.0.1) thread_safe (0.3.6) - tilt (2.0.10) tzinfo (1.2.7) thread_safe (~> 0.1) unf (0.1.4) unf_ext unf_ext (0.0.7.7) - unf_ext (0.0.7.7-x64-mingw32) unicode-display_width (1.7.0) unicorn (5.5.5) kgio (~> 2.6) @@ -403,28 +361,22 @@ GEM unicorn-worker-killer (0.4.4) get_process_mem (~> 0) unicorn (>= 4, < 6) - webmock (3.8.3) - addressable (>= 2.3.6) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) webpacker (5.1.1) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webrobots (0.1.2) - websocket-driver (0.7.2) + websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) chronic (>= 0.6.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.3.0) + zeitwerk (2.3.1) PLATFORMS ruby - x64-mingw32 DEPENDENCIES activemodel-serializers-xml @@ -440,6 +392,7 @@ DEPENDENCIES capistrano-rbenv capistrano3-unicorn capybara + codecov daemons delayed_job delayed_job_active_record @@ -450,12 +403,9 @@ DEPENDENCIES ffaker flamegraph http - httparty ipaddress_2 - jquery-rails listen mail - mechanize memoist memory_profiler meta_request @@ -465,7 +415,7 @@ DEPENDENCIES mock_redis net-sftp newrelic_rpm - nokogiri (~> 1.10.9) + nokogiri oauth2 pg pry-byebug @@ -480,7 +430,6 @@ DEPENDENCIES redis request_store responders - retriable rubocop rubocop-rails ruby-vips @@ -491,14 +440,12 @@ DEPENDENCIES shoulda-context shoulda-matchers simple_form - simplecov (~> 0.17.0) - sinatra + simplecov stackprof streamio-ffmpeg stripe unicorn unicorn-worker-killer - webmock webpacker (>= 4.0.x) whenever diff --git a/INSTALL.debian b/INSTALL.debian index a6f3d6736..c435d7748 100644 --- a/INSTALL.debian +++ b/INSTALL.debian @@ -32,9 +32,6 @@ if [[ -z "$HOSTNAME" ]] ; then exit 1 fi -echo -n "* Enter the VLAN IP address for this server (ex: 172.16.0.1, enter nothing to skip): " -read VLAN_IP_ADDR - # Install packages echo "* Installing packages..." @@ -52,17 +49,6 @@ apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt- 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 -# vrack specific stuff -if [ -n "$VLAN_IP_ADDR" ] ; then - apt-get -y install vlan - modprobe 8021q - echo "8021q" >> /etc/modules - vconfig add eno2 99 - ip addr add $VLAN_IP_ADDR/24 dev eno2.99 - ip link set up eno2.99 - curl -L -s $GITHUB_INSTALL_SCRIPTS/vrack-cfg.yaml -o /etc/netplan/01-netcfg.yaml -fi - 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 curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash - diff --git a/README.md b/README.md index 6685ad20b..b6834fdff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Test Coverage](https://api.codeclimate.com/v1/badges/a51f46fb460a35104d82/test_coverage)](https://codeclimate.com/github/danbooru/danbooru/test_coverage) [![Discord](https://img.shields.io/discord/310432830138089472?label=Discord)](https://discord.gg/eSVKkUF) +[![codecov](https://codecov.io/gh/danbooru/danbooru/branch/master/graph/badge.svg)](https://codecov.io/gh/danbooru/danbooru) [![Discord](https://img.shields.io/discord/310432830138089472?label=Discord)](https://discord.gg/eSVKkUF) ## Installation diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a0825ee0d..9afec6b03 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -88,6 +88,8 @@ class ApplicationController < ActionController::Base def rescue_exception(exception) case exception + when ActionView::Template::Error + rescue_exception(exception.cause) when ActiveRecord::QueryCanceled render_error_page(500, exception, template: "static/search_timeout", message: "The database timed out running your query.") when ActionController::BadRequest diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb new file mode 100644 index 000000000..37fa3565b --- /dev/null +++ b/app/controllers/autocomplete_controller.rb @@ -0,0 +1,16 @@ +class AutocompleteController < ApplicationController + respond_to :xml, :json + + def index + @tags = Tag.names_matches_with_aliases(params[:query], params.fetch(:limit, 10).to_i) + + if request.variant.opensearch? + expires_in 1.hour + results = [params[:query], @tags.map(&:pretty_name)] + respond_with(results) + else + # XXX + respond_with(@tags.map(&:attributes)) + end + end +end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index d1b368514..06c186081 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -97,6 +97,7 @@ class CommentsController < ApplicationController if request.format.atom? @comments = @comments.includes(:creator, :post) + @comments = @comments.select { |comment| comment.post.visible? } elsif request.format.html? @comments = @comments.includes(:creator, :updater, post: :uploader) @comments = @comments.includes(:votes) if CurrentUser.is_member? diff --git a/app/controllers/explore/posts_controller.rb b/app/controllers/explore/posts_controller.rb index c2d7e25c3..f733bf6e1 100644 --- a/app/controllers/explore/posts_controller.rb +++ b/app/controllers/explore/posts_controller.rb @@ -22,23 +22,25 @@ module Explore def viewed @date, @scale, @min_date, @max_date = parse_date(params) - @posts = PostViewCountService.new.popular_posts(@date) + @posts = ReportbooruService.new.popular_posts(@date) respond_with(@posts) end def searches @date, @scale, @min_date, @max_date = parse_date(params) - @search_service = PopularSearchService.new(@date) + @searches = ReportbooruService.new.post_search_rankings(@date) + respond_with(@searches) end def missed_searches - @search_service = MissedSearchService.new + @missed_searches = ReportbooruService.new.missed_search_rankings + respond_with(@missed_searches) end private def parse_date(params) - date = params[:date].present? ? Date.parse(params[:date]) : Time.zone.today + date = params[:date].present? ? Date.parse(params[:date]) : Date.today scale = params[:scale].in?(["day", "week", "month"]) ? params[:scale] : "day" min_date = date.send("beginning_of_#{scale}") max_date = date.send("next_#{scale}").send("beginning_of_#{scale}") diff --git a/app/controllers/iqdb_queries_controller.rb b/app/controllers/iqdb_queries_controller.rb index 21a063838..659c22950 100644 --- a/app/controllers/iqdb_queries_controller.rb +++ b/app/controllers/iqdb_queries_controller.rb @@ -5,7 +5,7 @@ class IqdbQueriesController < ApplicationController # XXX allow bare search params for backwards compatibility. search_params.merge!(params.slice(:url, :image_url, :file_url, :post_id, :limit, :similarity, :high_similarity).permit!) - @high_similarity_matches, @low_similarity_matches, @matches = IqdbProxy.search(search_params) + @high_similarity_matches, @low_similarity_matches, @matches = IqdbProxy.new.search(search_params) respond_with(@matches, template: "iqdb_queries/show") end diff --git a/app/controllers/mock_services_controller.rb b/app/controllers/mock_services_controller.rb new file mode 100644 index 000000000..67d387fb6 --- /dev/null +++ b/app/controllers/mock_services_controller.rb @@ -0,0 +1,48 @@ +class MockServicesController < ApplicationController + skip_forgery_protection + respond_to :json + + before_action do + raise User::PrivilegeError if Rails.env.production? + end + + def recommender_recommend + @data = posts.map { |post| [post.id, rand(0.0..1.0)] } + render json: @data + end + + def recommender_similar + @data = posts.map { |post| [post.id, rand(0.0..1.0)] } + render json: @data + end + + def reportbooru_missed_searches + @data = tags.map { |tag| "#{tag.name} #{rand(1.0..1000.0)}" }.join("\n") + render json: @data + end + + def reportbooru_post_searches + @data = tags.map { |tag| [tag.name, rand(1..1000)] } + render json: @data + end + + def reportbooru_post_views + @data = posts.map { |post| [post.id, rand(1..1000)] } + render json: @data + end + + def iqdbs_similar + @data = posts.map { |post| { post_id: post.id, score: rand(0..100)} } + render json: @data + end + + private + + def posts(limit = 10) + Post.last(limit) + end + + def tags(limit = 10) + Tag.order(post_count: :desc).limit(limit) + end +end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index fc151ebd7..709be20f3 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,4 +1,7 @@ class StaticController < ApplicationController + def privacy_policy + end + def terms_of_service end @@ -13,13 +16,40 @@ class StaticController < ApplicationController redirect_to wiki_page_path("help:dtext") unless request.format.js? end + def opensearch + end + def site_map end - def sitemap - @popular_search_service = PopularSearchService.new(Date.yesterday) - @posts = Post.where("created_at > ?", 1.week.ago).order(score: :desc).limit(200) - @posts = @posts.select(&:visible?) - render layout: false + def sitemap_index + @sitemap = params[:sitemap] + @limit = params.fetch(:limit, 10000).to_i + + case @sitemap + when "artists" + @relation = Artist.undeleted + @search = { is_deleted: "false" } + when "forum_topics" + @relation = ForumTopic.undeleted + @search = { is_deleted: "false" } + when "pools" + @relation = Pool.undeleted + @search = { is_deleted: "false" } + when "posts" + @relation = Post.order(id: :asc) + @serach = {} + when "tags" + @relation = Tag.nonempty + @search = {} + when "users" + @relation = User.all + @search = {} + when "wiki_pages" + @relation = WikiPage.undeleted + @search = { is_deleted: "false" } + else + raise NotImplementedError + end end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index d99d58125..97f5090dc 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -21,7 +21,7 @@ class UploadsController < ApplicationController def image_proxy authorize Upload resp = ImageProxy.get_image(params[:url]) - send_data resp.body, :type => resp.content_type, :disposition => "inline" + send_data resp.body, type: resp.mime_type, disposition: "inline" end def index diff --git a/app/controllers/user_name_change_requests_controller.rb b/app/controllers/user_name_change_requests_controller.rb index e93278492..2dfc96b2e 100644 --- a/app/controllers/user_name_change_requests_controller.rb +++ b/app/controllers/user_name_change_requests_controller.rb @@ -19,7 +19,7 @@ class UserNameChangeRequestsController < ApplicationController end def index - @change_requests = authorize UserNameChangeRequest.visible(CurrentUser.user).order("id desc").paginate(params[:page], :limit => params[:limit]) + @change_requests = authorize UserNameChangeRequest.visible(CurrentUser.user).paginated_search(params) respond_with(@change_requests) end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7930cf76c..bb6f5bb98 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -27,7 +27,7 @@ class UsersController < ApplicationController def index if params[:name].present? @user = User.find_by_name!(params[:name]) - redirect_to user_path(@user) + redirect_to user_path(@user, variant: params[:variant]) return end @@ -42,7 +42,9 @@ class UsersController < ApplicationController def show @user = authorize User.find(params[:id]) - respond_with(@user, methods: @user.full_attributes) + respond_with(@user, methods: @user.full_attributes) do |format| + format.html.tooltip { render layout: false } + end end def profile diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 26a4a0b59..b33c763d1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -121,6 +121,10 @@ module ApplicationHelper raw content_tag(:time, duration, datetime: datetime, title: title) end + def humanized_number(number) + number_to_human number, units: { thousand: "k", million: "m" }, format: "%n%u" + end + def time_ago_in_words_tagged(time, compact: false) if time.nil? tag.em(tag.time("unknown")) @@ -162,8 +166,10 @@ module ApplicationHelper end end - def link_to_ip(ip) - link_to ip, ip_addresses_path(search: { ip_addr: ip, group_by: "user" }) + def link_to_ip(ip, shorten: false, **options) + ip_addr = IPAddr.new(ip.to_s) + ip_addr.prefix = 64 if ip_addr.ipv6? && shorten + link_to ip_addr.to_s, ip_addresses_path(search: { ip_addr: ip, group_by: "user" }), **options end def link_to_search(search) @@ -186,12 +192,13 @@ module ApplicationHelper def link_to_user(user) return "anonymous" if user.blank? - user_class = "user-#{user.level_string.downcase}" + user_class = "user user-#{user.level_string.downcase}" user_class += " user-post-approver" if user.can_approve_posts? user_class += " user-post-uploader" if user.can_upload_free? user_class += " user-banned" if user.is_banned? - user_class += " with-style" if CurrentUser.user.style_usernames? - link_to(user.pretty_name, user_path(user), :class => user_class) + + 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) end def mod_link_to_user(user, positive_or_negative) @@ -217,21 +224,8 @@ module ApplicationHelper tag.div(text, class: "prose", **options) end - def dtext_field(object, name, options = {}) - options[:name] ||= name.capitalize - options[:input_id] ||= "#{object}_#{name}" - options[:input_name] ||= "#{object}[#{name}]" - options[:value] ||= instance_variable_get("@#{object}").try(name) - options[:preview_id] ||= "dtext-preview" - options[:classes] ||= "" - options[:hint] ||= "" - options[:type] ||= "text" - - render "dtext/form", options - end - - def dtext_preview_button(object, name, input_id: "#{object}_#{name}", preview_id: "dtext-preview") - tag.input value: "Preview", type: "button", class: "dtext-preview-button", "data-input-id": input_id, "data-preview-id": preview_id + def dtext_preview_button(preview_field) + tag.input value: "Preview", type: "button", class: "dtext-preview-button", "data-preview-field": preview_field end def quick_search_form_for(attribute, url, name, autocomplete: nil, redirect: false, &block) @@ -266,7 +260,7 @@ module ApplicationHelper def body_attributes(user, params, current_item = nil) current_user_data_attributes = data_attributes_for(user, "current-user", current_user_attributes) - if current_item.present? && current_item.respond_to?(:html_data_attributes) && current_item.respond_to?(:model_name) + if !current_item.nil? && current_item.respond_to?(:html_data_attributes) && current_item.respond_to?(:model_name) model_name = current_item.model_name.singular.dasherize model_attributes = current_item.html_data_attributes current_item_data_attributes = data_attributes_for(current_item, model_name, model_attributes) @@ -353,6 +347,19 @@ module ApplicationHelper end end + def canonical_url(url = nil) + if url.present? + content_for(:canonical_url) { url } + elsif content_for(:canonical_url).present? + content_for(:canonical_url) + else + request_params = request.params.sort.to_h.with_indifferent_access + request_params.delete(:page) if request_params[:page].to_i == 1 + request_params.delete(:limit) + url_for(**request_params, host: Danbooru.config.hostname, only_path: false) + end + end + def atom_feed_tag(title, url = {}) content_for(:html_header, auto_discovery_link_tag(:atom, url, title: title)) end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 1930adf07..7e11f41a7 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -20,8 +20,8 @@ module PaginationHelper params[:page] =~ /[ab]/ || records.current_page >= Danbooru.config.max_numbered_pages end - def numbered_paginator(records, switch_to_sequential = true) - if use_sequential_paginator?(records) && switch_to_sequential + def numbered_paginator(records) + if use_sequential_paginator?(records) return sequential_paginator(records) end diff --git a/app/helpers/seo_helper.rb b/app/helpers/seo_helper.rb new file mode 100644 index 000000000..a26d7762d --- /dev/null +++ b/app/helpers/seo_helper.rb @@ -0,0 +1,65 @@ +# https://yoast.com/structured-data-schema-ultimate-guide/ +# https://technicalseo.com/tools/schema-markup-generator/ +# https://developers.google.com/search/docs/data-types/sitelinks-searchbox +# https://developers.google.com/search/docs/data-types/logo +# https://search.google.com/structured-data/testing-tool/u/0/ +# https://search.google.com/test/rich-results +# https://schema.org/Organization +# https://schema.org/WebSite + +module SeoHelper + def site_description + "#{Danbooru.config.canonical_app_name} is the original anime image booru. Search millions of anime pictures categorized by thousands of tags." + end + + # https://developers.google.com/search/docs/data-types/video#video-object + def json_ld_video_data(post) + json_ld_tag({ + "@context": "https://schema.org", + "@type": "VideoObject", + "name": page_title, + "description": meta_description, + "uploadDate": post.created_at.iso8601, + "thumbnailUrl": post.preview_file_url, + "contentUrl": post.file_url, + }) + end + + def json_ld_website_data + urls = [ + Danbooru.config.twitter_url, + Danbooru.config.discord_server_url, + Danbooru.config.source_code_url, + "https://en.wikipedia.org/wiki/Danbooru" + ].compact + + json_ld_tag({ + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + url: root_url(host: Danbooru.config.hostname), + name: Danbooru.config.app_name, + logo: "#{root_url(host: Danbooru.config.hostname)}images/danbooru-logo-500x500.png", + sameAs: urls + }, + { + "@type": "WebSite", + "@id": root_url(anchor: "website", host: Danbooru.config.hostname), + "url": root_url(host: Danbooru.config.hostname), + "name": Danbooru.config.app_name, + "description": site_description, + "potentialAction": [{ + "@type": "SearchAction", + "target": "#{posts_url(host: Danbooru.config.hostname)}?tags={search_term_string}", + "query-input": "required name=search_term_string" + }] + } + ] + }) + end + + def json_ld_tag(data) + tag.script(data.to_json.html_safe, type: "application/ld+json") + end +end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 519fd03fa..4b00f95c2 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -6,12 +6,12 @@ function importAll(r) { require('@rails/ujs').start(); require('hammerjs'); -require('stupid-table-plugin'); require('jquery-hotkeys'); // should start looking for nodejs replacements importAll(require.context('../vendor', true, /\.js$/)); +require('jquery'); require("jquery-ui/ui/effects/effect-shake"); require("jquery-ui/ui/widgets/autocomplete"); require("jquery-ui/ui/widgets/button"); @@ -33,6 +33,7 @@ require("@fortawesome/fontawesome-free/css/regular.css"); importAll(require.context('../src/javascripts', true, /\.js(\.erb)?$/)); importAll(require.context('../src/styles', true, /\.s?css(?:\.erb)?$/)); +export { default as jQuery } from "jquery"; export { default as Autocomplete } from '../src/javascripts/autocomplete.js.erb'; export { default as Blacklist } from '../src/javascripts/blacklists.js'; export { default as Comment } from '../src/javascripts/comments.js'; @@ -47,5 +48,6 @@ export { default as PostVersion } from '../src/javascripts/post_version.js'; export { default as RelatedTag } from '../src/javascripts/related_tag.js'; export { default as Shortcuts } from '../src/javascripts/shortcuts.js'; export { default as Upload } from '../src/javascripts/uploads.js.erb'; +export { default as UserTooltip } from '../src/javascripts/user_tooltips.js'; export { default as Utility } from '../src/javascripts/utility.js'; export { default as Ugoira } from '../src/javascripts/ugoira.js'; diff --git a/app/javascript/src/javascripts/autocomplete.js.erb b/app/javascript/src/javascripts/autocomplete.js.erb index 28f44f989..ae493b63b 100644 --- a/app/javascript/src/javascripts/autocomplete.js.erb +++ b/app/javascript/src/javascripts/autocomplete.js.erb @@ -9,6 +9,7 @@ Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_ Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>; /* eslint-enable */ +Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"]; 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"); @@ -37,7 +38,7 @@ Autocomplete.initialize_all = function() { }); this.initialize_tag_autocomplete(); - this.initialize_mention_autocomplete($(".autocomplete-mentions textarea")); + this.initialize_mention_autocomplete($("form div.input.dtext textarea")); this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source); this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source); this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source); @@ -248,9 +249,6 @@ Autocomplete.render_item = function(list, item) { } else if (item.type === "user") { var level_class = "user-" + item.level.toLowerCase(); $link.addClass(level_class); - if (CurrentUser.data("style-usernames")) { - $link.addClass("with-style"); - } } else if (item.type === "pool") { $link.addClass("pool-category-" + item.category); } @@ -268,9 +266,7 @@ Autocomplete.render_item = function(list, item) { Autocomplete.static_metatags = { order: Autocomplete.ORDER_METATAGS, - status: [ - "any", "deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated" - ], + status: ["any"].concat(Autocomplete.MISC_STATUSES), rating: [ "safe", "questionable", "explicit" ], @@ -280,12 +276,8 @@ Autocomplete.static_metatags = { embedded: [ "true", "false" ], - child: [ - "any", "none" - ], - parent: [ - "any", "none" - ], + child: ["any", "none"].concat(Autocomplete.MISC_STATUSES), + parent: ["any", "none"].concat(Autocomplete.MISC_STATUSES), filetype: [ "jpg", "png", "gif", "swf", "zip", "webm", "mp4" ], diff --git a/app/javascript/src/javascripts/dtext.js b/app/javascript/src/javascripts/dtext.js index 94d708f79..8b9edbb60 100644 --- a/app/javascript/src/javascripts/dtext.js +++ b/app/javascript/src/javascripts/dtext.js @@ -39,8 +39,11 @@ Dtext.call_edit = function(e, $button, $input, $preview) { Dtext.click_button = function(e) { var $button = $(e.target); - var $input = $("#" + $button.data("input-id")); - var $preview = $("#" + $button.data("preview-id")); + var $form = $button.parents("form"); + var fieldName = $button.data("preview-field"); + var $inputContainer = $form.find(`div.input.${fieldName} .dtext-previewable`); + var $input = $inputContainer.find("> input, > textarea"); + var $preview = $inputContainer.find("div.dtext-preview"); if ($button.val().match(/preview/i)) { Dtext.call_preview(e, $button, $input, $preview); diff --git a/app/javascript/src/javascripts/forum_posts.js b/app/javascript/src/javascripts/forum_posts.js index 9b8f7ef0c..28437c5df 100644 --- a/app/javascript/src/javascripts/forum_posts.js +++ b/app/javascript/src/javascripts/forum_posts.js @@ -7,17 +7,15 @@ ForumPost.initialize_all = function() { } ForumPost.initialize_edit_links = function() { - $(".edit_forum_post_link").on("click.danbooru", function(e) { - var link_id = $(this).attr("id"); - var forum_post_id = link_id.match(/^edit_forum_post_link_(\d+)$/)[1]; - $("#edit_forum_post_" + forum_post_id).fadeToggle("fast"); + $(document).on("click.danbooru", ".edit_forum_post_link", function(e) { + let $form = $(this).parents("article.forum-post").find("form.edit_forum_post"); + $form.fadeToggle("fast"); e.preventDefault(); }); - $(".edit_forum_topic_link").on("click.danbooru", function(e) { - var link_id = $(this).attr("id"); - var forum_topic_id = link_id.match(/^edit_forum_topic_link_(\d+)$/)[1]; - $("#edit_forum_topic_" + forum_topic_id).fadeToggle("fast"); + $(document).on("click.danbooru", ".edit_forum_topic_link", function(e) { + let $form = $(this).parents("article.forum-post").find("form.edit_forum_topic"); + $form.fadeToggle("fast"); e.preventDefault(); }); diff --git a/app/javascript/src/javascripts/pools.js b/app/javascript/src/javascripts/pools.js index 4134056a4..e9fce468d 100644 --- a/app/javascript/src/javascripts/pools.js +++ b/app/javascript/src/javascripts/pools.js @@ -20,11 +20,6 @@ Pool.initialize_add_to_pool_link = function() { e.preventDefault(); $("#add-to-pool-dialog").dialog("open"); }); - - $("#recent-pools li").on("click.danbooru", function(e) { - e.preventDefault(); - $("#pool_name").val($(this).attr("data-value")); - }); } Pool.initialize_simple_edit = function() { diff --git a/app/javascript/src/javascripts/post_mode_menu.js b/app/javascript/src/javascripts/post_mode_menu.js index ea0a474c4..328a50043 100644 --- a/app/javascript/src/javascripts/post_mode_menu.js +++ b/app/javascript/src/javascripts/post_mode_menu.js @@ -70,7 +70,7 @@ PostModeMenu.initialize_edit_form = function() { $(document).on("click.danbooru", "#quick-edit-form input[type=submit]", async function(e) { e.preventDefault(); - let post_id = $("#quick-edit-form").data("post-id"); + let post_id = $("#quick-edit-form").attr("data-post-id"); await Post.update(post_id, "quick-edit", { post: { tag_string: $("#post_tag_string").val() }}); }); } diff --git a/app/javascript/src/javascripts/post_tooltips.js b/app/javascript/src/javascripts/post_tooltips.js index 4be1754ec..8daf42234 100644 --- a/app/javascript/src/javascripts/post_tooltips.js +++ b/app/javascript/src/javascripts/post_tooltips.js @@ -1,28 +1,76 @@ -import CurrentUser from './current_user' -import Utility from './utility' - -require('qtip2'); -require('qtip2/dist/jquery.qtip.css'); +import CurrentUser from './current_user'; +import Utility from './utility'; +import { delegate, hideAll } from 'tippy.js'; +import 'tippy.js/dist/tippy.css'; let PostTooltip = {}; -PostTooltip.render_tooltip = async function (event, qtip) { +PostTooltip.POST_SELECTOR = "*:not(.ui-sortable-handle) > .post-preview img, .dtext-post-id-link"; +PostTooltip.SHOW_DELAY = 500; +PostTooltip.HIDE_DELAY = 125; +PostTooltip.DURATION = 250; + +PostTooltip.initialize = function () { + if (PostTooltip.disabled()) { + return; + } + + delegate("body", { + allowHTML: true, + appendTo: document.body, + delay: [PostTooltip.SHOW_DELAY, PostTooltip.HIDE_DELAY], + duration: PostTooltip.DURATION, + interactive: true, + maxWidth: "none", + target: PostTooltip.POST_SELECTOR, + theme: "common-tooltip post-tooltip", + touch: false, + + onCreate: PostTooltip.on_create, + onShow: PostTooltip.on_show, + onHide: PostTooltip.on_hide, + }); + + $(document).on("click.danbooru.postTooltip", ".post-tooltip-disable", PostTooltip.on_disable_tooltips); +}; + +PostTooltip.on_create = function (instance) { + let title = instance.reference.getAttribute("title"); + + if (title) { + instance.reference.setAttribute("data-title", title); + instance.reference.setAttribute("title", ""); + } +}; + +PostTooltip.on_show = async function (instance) { let post_id = null; let preview = false; + let $target = $(instance.reference); + let $tooltip = $(instance.popper); - if ($(this).is(".dtext-post-id-link")) { + hideAll({ exclude: instance }); + + // skip if tooltip has already been rendered. + if ($tooltip.has(".post-tooltip-body").length) { + return; + } + + if ($target.is(".dtext-post-id-link")) { preview = true; - post_id = /\/posts\/(\d+)/.exec($(this).attr("href"))[1]; + post_id = /\/posts\/(\d+)/.exec($target.attr("href"))[1]; } else { - post_id = $(this).parents("[data-id]").data("id"); + post_id = $target.parents("[data-id]").data("id"); } try { - qtip.cache.request = $.get(`/posts/${post_id}`, { variant: "tooltip", preview: preview }); - let html = await qtip.cache.request; + $tooltip.addClass("tooltip-loading"); - qtip.set("content.text", html); - qtip.elements.tooltip.removeClass("post-tooltip-loading"); + instance._request = $.get(`/posts/${post_id}`, { variant: "tooltip", preview: preview }); + let html = await instance._request; + instance.setContent(html); + + $tooltip.removeClass("tooltip-loading"); } catch (error) { if (error.status !== 0 && error.statusText !== "abort") { Utility.error(`Error displaying tooltip for post #${post_id} (error: ${error.status} ${error.statusText})`); @@ -30,98 +78,19 @@ PostTooltip.render_tooltip = async function (event, qtip) { } }; -// Hide the tooltip the first time it is shown, while we wait on the ajax call to complete. -PostTooltip.on_show = function (event, qtip) { - if (!qtip.cache.hasBeenShown) { - qtip.elements.tooltip.addClass("post-tooltip-loading"); - qtip.cache.hasBeenShown = true; +PostTooltip.on_hide = function (instance) { + if (instance._request?.state() === "pending") { + instance._request.abort(); } -}; - -PostTooltip.POST_SELECTOR = "*:not(.ui-sortable-handle) > .post-preview img, .dtext-post-id-link"; - -// http://qtip2.com/options -PostTooltip.QTIP_OPTIONS = { - style: { - classes: "qtip-light post-tooltip", - tip: false - }, - content: PostTooltip.render_tooltip, - overwrite: false, - position: { - viewport: true, - my: "bottom left", - at: "top left", - effect: false, - adjust: { - y: -2, - method: "shift", - }, - }, - show: { - solo: true, - delay: 750, - effect: false, - ready: true, - event: "mouseenter", - }, - hide: { - delay: 250, - fixed: true, - effect: false, - event: "unfocus click mouseleave", - }, - events: { - show: PostTooltip.on_show, - }, -}; - -PostTooltip.initialize = function () { - $(document).on("mouseenter.danbooru.postTooltip", PostTooltip.POST_SELECTOR, function (event) { - if (PostTooltip.disabled()) { - $(this).qtip("disable"); - } else { - $(this).qtip(PostTooltip.QTIP_OPTIONS, event); - } - }); - - // Cancel pending ajax requests when we mouse out of the thumbnail. - $(document).on("mouseleave.danbooru.postTooltip", PostTooltip.POST_SELECTOR, function (event) { - let qtip = $(event.target).qtip("api"); - - if (qtip && qtip.cache && qtip.cache.request && qtip.cache.request.state() === "pending") { - qtip.cache.request.abort(); - } - }); - - $(document).on("click.danbooru.postTooltip", ".post-tooltip-disable", PostTooltip.on_disable_tooltips); - - // Hide tooltips when pressing keys or clicking thumbnails. - $(document).on("keydown.danbooru.postTooltip", PostTooltip.hide); - $(document).on("click.danbooru.postTooltip", PostTooltip.POST_SELECTOR, PostTooltip.hide); - - // Disable tooltips on touch devices. https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent - PostTooltip.isTouching = false; - $(document).on("touchstart.danbooru.postTooltip", function (event) { PostTooltip.isTouching = true; }); - $(document).on("touchend.danbooru.postTooltip", function (event) { PostTooltip.isTouching = false; }); -}; - -PostTooltip.hide = function (event) { - // Hide on any key except control (user may be control-clicking link inside tooltip). - if (event.type === "keydown" && event.ctrlKey === true) { - return; - } - - $(".post-tooltip:visible").qtip("hide"); -}; +} PostTooltip.disabled = function (event) { - return PostTooltip.isTouching || CurrentUser.data("disable-post-tooltips"); + return CurrentUser.data("disable-post-tooltips"); }; PostTooltip.on_disable_tooltips = async function (event) { event.preventDefault(); - $(event.target).parents(".qtip").qtip("hide"); + hideAll(); if (CurrentUser.data("is-anonymous")) { Utility.notice('You must login to disable tooltips'); diff --git a/app/javascript/src/javascripts/posts.js.erb b/app/javascript/src/javascripts/posts.js.erb index cdf7b9aaf..9eef63e88 100644 --- a/app/javascript/src/javascripts/posts.js.erb +++ b/app/javascript/src/javascripts/posts.js.erb @@ -300,10 +300,10 @@ Post.initialize_favlist = function() { }); } -Post.view_original = function(e) { +Post.view_original = function(e = null) { if (Utility.test_max_width(660)) { // Do the default behavior (navigate to image) - return false; + return; } var $image = $("#image"); @@ -316,13 +316,13 @@ Post.view_original = function(e) { }); Note.Box.scale_all(); $("body").attr("data-post-current-image-size", "original"); - return false; + e?.preventDefault(); } -Post.view_large = function(e) { +Post.view_large = function(e = null) { if (Utility.test_max_width(660)) { // Do the default behavior (navigate to image) - return false; + return; } var $image = $("#image"); @@ -335,7 +335,7 @@ Post.view_large = function(e) { }); Note.Box.scale_all(); $("body").attr("data-post-current-image-size", "large"); - return false; + e?.preventDefault(); } Post.toggle_fit_window = function(e) { diff --git a/app/javascript/src/javascripts/saved_searches.js b/app/javascript/src/javascripts/saved_searches.js deleted file mode 100644 index 2425265dc..000000000 --- a/app/javascript/src/javascripts/saved_searches.js +++ /dev/null @@ -1,11 +0,0 @@ -let SavedSearch = {}; - -SavedSearch.initialize_all = function() { - if ($("#c-saved-searches").length) { - $("#c-saved-searches table").stupidtable(); - } -} - -$(SavedSearch.initialize_all); - -export default SavedSearch diff --git a/app/javascript/src/javascripts/user_tooltips.js b/app/javascript/src/javascripts/user_tooltips.js new file mode 100644 index 000000000..cc0a9f3b9 --- /dev/null +++ b/app/javascript/src/javascripts/user_tooltips.js @@ -0,0 +1,85 @@ +import Utility from './utility'; +import { delegate } from 'tippy.js'; +import 'tippy.js/dist/tippy.css'; + +let UserTooltip = {}; + +UserTooltip.SELECTOR = "*:not(.user-tooltip-name) > a.user, a.dtext-user-id-link, a.dtext-user-mention-link"; +UserTooltip.SHOW_DELAY = 500; +UserTooltip.HIDE_DELAY = 125; +UserTooltip.DURATION = 250; +UserTooltip.MAX_WIDTH = 600; + +UserTooltip.initialize = function () { + delegate("body", { + allowHTML: true, + appendTo: document.body, + delay: [UserTooltip.SHOW_DELAY, UserTooltip.HIDE_DELAY], + duration: UserTooltip.DURATION, + interactive: true, + maxWidth: UserTooltip.MAX_WIDTH, + target: UserTooltip.SELECTOR, + theme: "common-tooltip user-tooltip", + touch: false, + + onShow: UserTooltip.on_show, + onHide: UserTooltip.on_hide, + }); + + delegate("body", { + allowHTML: true, + interactive: true, + theme: "common-tooltip", + target: ".user-tooltip-menu-button", + placement: "bottom", + touch: false, + trigger: "click", + content: (element) => { + return $(element).parents(".user-tooltip").find(".user-tooltip-menu").get(0); + } + }); +}; + +UserTooltip.on_show = async function (instance) { + let $target = $(instance.reference); + let $tooltip = $(instance.popper); + + // skip if tooltip has already been rendered. + if ($tooltip.has(".user-tooltip-body").length) { + return; + } + + try { + $tooltip.addClass("tooltip-loading"); + + if ($target.is("a.dtext-user-id-link")) { + let user_id = /\/users\/(\d+)/.exec($target.attr("href"))[1]; + instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" }); + } else if ($target.is("a.user")) { + let user_id = $target.attr("data-user-id"); + instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" }); + } else if ($target.is("a.dtext-user-mention-link")) { + let user_name = $target.attr("data-user-name"); + instance._request = $.get(`/users`, { name: user_name, variant: "tooltip" }); + } + + let html = await instance._request; + instance.setContent(html); + + $tooltip.removeClass("tooltip-loading"); + } catch (error) { + if (error.status !== 0 && error.statusText !== "abort") { + Utility.error(`Error displaying tooltip (error: ${error.status} ${error.statusText})`); + } + } +}; + +UserTooltip.on_hide = function (instance) { + if (instance._request?.state() === "pending") { + instance._request.abort(); + } +} + +$(document).ready(UserTooltip.initialize); + +export default UserTooltip diff --git a/app/javascript/src/styles/base/020_base.scss b/app/javascript/src/styles/base/020_base.scss index 4dbfee03a..352f46349 100644 --- a/app/javascript/src/styles/base/020_base.scss +++ b/app/javascript/src/styles/base/020_base.scss @@ -129,6 +129,14 @@ table tfoot { font-size: 0.9em; } +a.link-plain { + color: unset; + + &:hover { + text-decoration: underline; + } +} + .fixed-width-container { max-width: 70em; } diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index ab97b39f3..211b835fc 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -2,6 +2,7 @@ --body-background-color: white; --text-color: hsl(0, 0%, 15%); + --inverse-text-color: white; --muted-text-color: hsl(0, 0%, 55%); --header-color: hsl(0, 0%, 15%); @@ -72,7 +73,8 @@ --comment-sticky-background-color: var(--subnav-menu-background-color); --post-tooltip-background-color: var(--body-background-color); - --post-tooltip-border-color: #767676; + --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-header-background-color: var(--subnav-menu-background-color); --post-tooltip-info-color: #555; --post-tooltip-scrollbar-background: #999999; @@ -81,6 +83,9 @@ --post-tooltip-scrollbar-track-background: #EEEEEE; --post-tooltip-scrollbar-track-border: 0 none white; + --user-tooltip-positive-feedback-color: orange; + --user-tooltip-negative-feedback-color: red; + --preview-current-post-background: rgba(0, 0, 0, 0.1); --autocomplete-selected-background-color: var(--subnav-menu-background-color); @@ -200,6 +205,7 @@ --user-platinum-color: gray; --user-gold-color: #00F; --user-member-color: var(--link-color); + --user-banned-color: black; --news-updates-background: #EEE; --news-updates-border: 2px solid #666; @@ -260,6 +266,7 @@ body[data-current-user-theme="dark"] { /* main text colors */ --text-color: var(--grey-5); + --inverse-text-color: white; --muted-text-color: var(--grey-4); --header-color: var(--grey-6); @@ -282,6 +289,7 @@ body[data-current-user-theme="dark"] { --collection-pool-color: var(--general-tag-color); --collection-pool-hover-color: var(--general-tag-hover-color); + --user-banned-color: var(--grey-1); --user-member-color: var(--blue-1); --user-gold-color: var(--yellow-1); --user-platinum-color: var(--grey-4); @@ -394,6 +402,9 @@ body[data-current-user-theme="dark"] { --post-tooltip-scrollbar-track-background: var(--grey-1); --post-tooltip-scrollbar-track-border: none; + --user-tooltip-positive-feedback-color: var(--yellow-1); + --user-tooltip-negative-feedback-color: var(--red-1); + --preview-pending-color: var(--blue-1); --preview-flagged-color: var(--red-1); --preview-deleted-color: var(--grey-5); diff --git a/app/javascript/src/styles/common/messages.scss b/app/javascript/src/styles/common/messages.scss index c25f27085..4d70c0103 100644 --- a/app/javascript/src/styles/common/messages.scss +++ b/app/javascript/src/styles/common/messages.scss @@ -17,7 +17,8 @@ div.list-of-messages { a.message-timestamp { font-style: italic; - color: var(--text-color); + font-size: 0.90em; + color: var(--muted-text-color); &:hover { text-decoration: underline; } } } diff --git a/app/javascript/src/styles/common/simple_form.scss b/app/javascript/src/styles/common/simple_form.scss index 9d39c3a05..2b1706df4 100644 --- a/app/javascript/src/styles/common/simple_form.scss +++ b/app/javascript/src/styles/common/simple_form.scss @@ -39,7 +39,7 @@ form.simple_form { padding-left: 1em; } - &.text { + &.text, &.dtext { .hint { padding-left: 0; display: block; diff --git a/app/javascript/src/styles/common/user_styles.scss b/app/javascript/src/styles/common/user_styles.scss index 8c92cba98..52cc6453f 100644 --- a/app/javascript/src/styles/common/user_styles.scss +++ b/app/javascript/src/styles/common/user_styles.scss @@ -1,23 +1,25 @@ -a.user-admin.with-style { - color: var(--user-admin-color); -} +body[data-current-user-style-usernames="true"] { + a.user-admin { + color: var(--user-admin-color); + } -a.user-moderator.with-style { - color: var(--user-moderator-color); -} + a.user-moderator { + color: var(--user-moderator-color); + } -a.user-builder.with-style { - color: var(--user-builder-color); -} + a.user-builder { + color: var(--user-builder-color); + } -a.user-platinum.with-style { - color: var(--user-platinum-color); -} + a.user-platinum { + color: var(--user-platinum-color); + } -a.user-gold.with-style { - color: var(--user-gold-color); -} + a.user-gold { + color: var(--user-gold-color); + } -a.user-member.with-style { - color: var(--user-member-color); + a.user-member { + color: var(--user-member-color); + } } diff --git a/app/javascript/src/styles/specific/common_tooltips.scss b/app/javascript/src/styles/specific/common_tooltips.scss new file mode 100644 index 000000000..f4a9dca65 --- /dev/null +++ b/app/javascript/src/styles/specific/common_tooltips.scss @@ -0,0 +1,51 @@ +div[data-tippy-root].tooltip-loading { + visibility: hidden !important; +} + +.tippy-box[data-theme~="common-tooltip"] { + box-sizing: border-box; + + border: 1px solid var(--post-tooltip-border-color); + border-radius: 4px; + + color: var(--text-color); + background-color: var(--post-tooltip-background-color); + background-clip: padding-box; + box-shadow: var(--post-tooltip-box-shadow); + + /* bordered arrow styling; see https://github.com/atomiks/tippyjs/blob/master/src/scss/themes/light-border.scss */ + &[data-placement^=bottom] { + > .tippy-arrow:before { + border-bottom-color: var(--post-tooltip-background-color); + bottom: 16px; + } + + > .tippy-arrow:after { + border-bottom-color: var(--post-tooltip-border-color); + border-width: 0 7px 7px; + top: -8px; + left: 1px; + } + } + + &[data-placement^=top] { + > .tippy-arrow:before { + border-top-color: var(--post-tooltip-background-color); + } + + > .tippy-arrow:after { + border-top-color: var(--post-tooltip-border-color); + border-width: 7px 7px 0; + top: 17px; + left: 1px; + } + } + + > .tippy-arrow:after { + border-color: transparent; + border-style: solid; + content: ""; + position: absolute; + z-index: -1; + } +} diff --git a/app/javascript/src/styles/specific/post_tooltips.scss b/app/javascript/src/styles/specific/post_tooltips.scss index 2a98dfc1c..7d947c54c 100644 --- a/app/javascript/src/styles/specific/post_tooltips.scss +++ b/app/javascript/src/styles/specific/post_tooltips.scss @@ -1,6 +1,5 @@ $tooltip-line-height: 16px; -$tooltip-body-height: $tooltip-line-height * 6; // 6 lines high. -$tooltip-width: 164px * 3 - 10; // 3 thumbnails wide. +$tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. @mixin thin-scrollbar { &::-webkit-scrollbar { @@ -46,16 +45,13 @@ $tooltip-width: 164px * 3 - 10; // 3 thumbnails wide. } } -.post-tooltip { - max-width: $tooltip-width; - min-width: $tooltip-width; - box-sizing: border-box; +.tippy-box[data-theme~="post-tooltip"] { + min-width: 20em; + max-width: 40em !important; font-size: 11px; line-height: $tooltip-line-height; - border-color: var(--post-tooltip-border-color); - background-color: var(--post-tooltip-background-color); - .qtip-content { + .tippy-content { padding: 0; > * { @@ -85,38 +81,32 @@ $tooltip-width: 164px * 3 - 10; // 3 thumbnails wide. .post-tooltip-body-right { flex: 1; } } - .post-tooltip-header { + div.post-tooltip-header { background-color: var(--post-tooltip-header-background-color); display: flex; white-space: nowrap; - overflow: hidden; + align-items: center; - .post-tooltip-header-left { - flex: 1; + .post-tooltip-info { + margin-right: 0.5em; + color: var(--post-tooltip-info-color); + font-size: 10px; + flex: 0; } - .post-tooltip-header-right { + a.user { + margin-right: 0.5em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; + max-width: 11em; + } + + .post-tooltip-source { flex: 1; text-align: right; } - - .fa-xs { - vertical-align: baseline; - } - - .post-tooltip-disable { - margin-left: 0.5em; - } } - - .post-tooltip-info { - margin-left: 0.5em; - color: var(--post-tooltip-info-color); - font-size: 10px; - } - } - - &.post-tooltip-loading { - visibility: hidden; } } diff --git a/app/javascript/src/styles/specific/privacy_policy.scss b/app/javascript/src/styles/specific/privacy_policy.scss new file mode 100644 index 000000000..e67ef1ce3 --- /dev/null +++ b/app/javascript/src/styles/specific/privacy_policy.scss @@ -0,0 +1,5 @@ +#c-static #a-privacy-policy { + .summary { + font-style: italic; + } +} diff --git a/app/javascript/src/styles/specific/user_tooltips.scss b/app/javascript/src/styles/specific/user_tooltips.scss new file mode 100644 index 000000000..b7133ef31 --- /dev/null +++ b/app/javascript/src/styles/specific/user_tooltips.scss @@ -0,0 +1,103 @@ +.tippy-box[data-theme~="user-tooltip"] { + line-height: 1.25em; + min-width: 350px; + + .user-tooltip-header { + margin-bottom: 1em; + display: grid; + grid: + "avatar header-top menu" + "avatar header-bottom menu" / + 32px 1fr 32px; + column-gap: 0.25em; + + .user-tooltip-avatar { + font-size: 32px; + grid-area: avatar; + align-self: center; + } + + .user-tooltip-header-top { + grid-area: header-top; + + .user-tooltip-badge { + color: var(--inverse-text-color); + font-size: 0.70em; + padding: 2px 4px; + margin-right: 0.25em; + border-radius: 3px; + + &.user-tooltip-badge-admin { background-color: var(--user-admin-color); } + &.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); } + &.user-tooltip-badge-approver { background-color: var(--user-builder-color); } + &.user-tooltip-badge-contributor { background-color: var(--user-builder-color); } + &.user-tooltip-badge-builder { background-color: var(--user-builder-color); } + &.user-tooltip-badge-platinum { background-color: var(--user-platinum-color); } + &.user-tooltip-badge-gold { background-color: var(--user-gold-color); } + &.user-tooltip-badge-member { background-color: var(--user-member-color); } + &.user-tooltip-badge-banned { background-color: var(--user-banned-color); } + + &.user-tooltip-badge-positive-feedback { + color: var(--user-tooltip-positive-feedback-color); + border: 1px solid; + } + + &.user-tooltip-badge-negative-feedback { + color: var(--user-tooltip-negative-feedback-color); + border: 1px solid; + } + } + } + + .user-tooltip-header-bottom { + grid-area: header-bottom; + color: var(--muted-text-color); + font-size: 0.75em; + } + + a.user-tooltip-menu-button { + color: var(--muted-text-color); + grid-area: menu; + align-self: center; + text-align: center; + width: 28px; + height: 28px; + line-height: 28px; + border-radius: 50%; + + &:hover { + background-color: var(--subnav-menu-background-color); + } + } + + > ul.user-tooltip-menu { + display: none; + } + + ul.user-tooltip-menu { + .icon { + width: 1.5em; + } + } + } + + .user-tooltip-stats { + display: grid; + grid: auto / repeat(3, 1fr); + column-gap: 1em; + row-gap: 0.5em; + + .user-tooltip-stat-item { + text-align: center; + + .user-tooltip-stat-value { + font-weight: bold; + } + + .user-tooltip-stat-name { + font-size: 0.90em; + color: var(--muted-text-color); + } + } + } +} diff --git a/app/logical/apng_inspector.rb b/app/logical/apng_inspector.rb index 4921c226a..68cc62c06 100644 --- a/app/logical/apng_inspector.rb +++ b/app/logical/apng_inspector.rb @@ -57,7 +57,7 @@ class APNGInspector # if we did, file is probably maliciously formed # fail gracefully without marking the file as corrupt chunks += 1 - if chunks > 100000 + if chunks > 100_000 iend_reached = true break end @@ -66,7 +66,8 @@ class APNGInspector file.seek(current_pos + chunk_len + 4, IO::SEEK_SET) end end - return iend_reached + + iend_reached end def inspect! @@ -105,6 +106,7 @@ class APNGInspector if framedata.nil? || framedata.length != 4 return -1 end - return framedata.unpack1("N".freeze) + + framedata.unpack1("N".freeze) end end diff --git a/app/logical/artist_finder.rb b/app/logical/artist_finder.rb index 3cf5c9581..306f05fc9 100644 --- a/app/logical/artist_finder.rb +++ b/app/logical/artist_finder.rb @@ -6,7 +6,7 @@ module ArtistFinder SITE_BLACKLIST = [ "artstation.com/artist", # http://www.artstation.com/artist/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 "bcyimg.com", "bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg @@ -52,7 +52,7 @@ module ArtistFinder "hentai-foundry.com", "hentai-foundry.com/pictures/user", # http://www.hentai-foundry.com/pictures/user/aaaninja/ "hentai-foundry.com/user", # http://www.hentai-foundry.com/user/aaaninja/profile - %r!pictures\.hentai-foundry\.com(?:/\w)?!i, # http://pictures.hentai-foundry.com/a/aaaninja/ + %r{pictures\.hentai-foundry\.com(?:/\w)?}i, # http://pictures.hentai-foundry.com/a/aaaninja/ "i.imgur.com", # http://i.imgur.com/Ic9q3.jpg "instagram.com", # http://www.instagram.com/serafleur.art/ "iwara.tv", @@ -62,13 +62,15 @@ module ArtistFinder "monappy.jp", "monappy.jp/u", # https://monappy.jp/u/abara_bone "mstdn.jp", # https://mstdn.jp/@oneb + "www.newgrounds.com", # https://jessxjess.newgrounds.com/ + "newgrounds.com/art/view/", # https://www.newgrounds.com/art/view/jessxjess/avatar-korra "nicoseiga.jp", "nicoseiga.jp/priv", # http://lohas.nicoseiga.jp/priv/2017365fb6cfbdf47ad26c7b6039feb218c5e2d4/1498430264/6820259 "nicovideo.jp", "nicovideo.jp/user", # http://www.nicovideo.jp/user/317609 "nicovideo.jp/user/illust", # http://seiga.nicovideo.jp/user/illust/29075429 "nijie.info", # http://nijie.info/members.php?id=15235 - %r!nijie\.info/nijie_picture!i, # http://pic03.nijie.info/nijie_picture/32243_20150609224803_0.png + %r{nijie\.info/nijie_picture}i, # http://pic03.nijie.info/nijie_picture/32243_20150609224803_0.png "patreon.com", # http://patreon.com/serafleur "pawoo.net", # https://pawoo.net/@148nasuka "pawoo.net/web/accounts", # https://pawoo.net/web/accounts/228341 @@ -120,7 +122,7 @@ module ArtistFinder SITE_BLACKLIST_REGEXP = Regexp.union(SITE_BLACKLIST.map do |domain| domain = Regexp.escape(domain) if domain.is_a?(String) - %r!\Ahttps?://(?:[a-zA-Z0-9_-]+\.)*#{domain}/\z!i + %r{\Ahttps?://(?:[a-zA-Z0-9_-]+\.)*#{domain}/\z}i end) def find_artists(url) @@ -128,7 +130,7 @@ module ArtistFinder artists = [] while artists.empty? && url.size > 10 - u = url.sub(/\/+$/, "") + "/" + u = url.sub(%r{/+$}, "") + "/" u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%' artists += Artist.joins(:urls).where(["artists.is_deleted = FALSE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all url = File.dirname(url) + "/" diff --git a/app/logical/bulk_update_request_processor.rb b/app/logical/bulk_update_request_processor.rb index 96212a72e..293320510 100644 --- a/app/logical/bulk_update_request_processor.rb +++ b/app/logical/bulk_update_request_processor.rb @@ -148,8 +148,6 @@ class BulkUpdateRequestProcessor end.join("\n") end - private - def self.is_tag_move_allowed?(antecedent_name, consequent_name) antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name)) consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name)) diff --git a/app/logical/cloudflare_service.rb b/app/logical/cloudflare_service.rb index 6a1161502..8cc1be195 100644 --- a/app/logical/cloudflare_service.rb +++ b/app/logical/cloudflare_service.rb @@ -9,15 +9,6 @@ class CloudflareService api_token.present? && zone.present? end - def ips(expiry: 24.hours) - response = Danbooru::Http.cache(expiry).get("https://api.cloudflare.com/client/v4/ips") - return [] if response.code != 200 - - result = response.parse["result"] - ips = result["ipv4_cidrs"] + result["ipv6_cidrs"] - ips.map { |ip| IPAddr.new(ip) } - end - def purge_cache(urls) return unless enabled? diff --git a/app/logical/concerns/has_bit_flags.rb b/app/logical/concerns/has_bit_flags.rb index 2b2e2a355..c05b6d54a 100644 --- a/app/logical/concerns/has_bit_flags.rb +++ b/app/logical/concerns/has_bit_flags.rb @@ -16,7 +16,7 @@ module HasBitFlags end define_method("#{attribute}=") do |val| - if val.to_s =~ /t|1|y/ + if val.to_s =~ /[t1y]/ send("#{field}=", send(field) | bit_flag) else send("#{field}=", send(field) & ~bit_flag) diff --git a/app/logical/concerns/mentionable.rb b/app/logical/concerns/mentionable.rb index f6dda15e9..f0a8e16ee 100644 --- a/app/logical/concerns/mentionable.rb +++ b/app/logical/concerns/mentionable.rb @@ -11,8 +11,6 @@ module Mentionable # - user_field def mentionable(options = {}) @mentionable_options = options - - message_field = mentionable_option(:message_field) after_save :queue_mention_messages end diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index 3b2996b54..640692263 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -89,7 +89,7 @@ module Searchable def where_array_count(attr, value) qualified_column = "cardinality(#{qualified_column_for(attr)})" range = PostQueryBuilder.new(nil).parse_range(value, :integer) - where_operator("cardinality(#{qualified_column_for(attr)})", *range) + where_operator(qualified_column, *range) end def search_boolean_attribute(attribute, params) @@ -170,7 +170,7 @@ module Searchable end end - def search_text_attribute(attr, params, **options) + def search_text_attribute(attr, params) if params[attr].present? where(attr => params[attr]) elsif params[:"#{attr}_eq"].present? @@ -279,7 +279,8 @@ module Searchable return find_ordered(parse_ids[1]) end end - return default_order + + default_order end def default_order diff --git a/app/logical/current_user.rb b/app/logical/current_user.rb index 292bf4a72..4c8d80349 100644 --- a/app/logical/current_user.rb +++ b/app/logical/current_user.rb @@ -24,15 +24,6 @@ class CurrentUser scoped(user, &block) end - def self.as_system(&block) - if block_given? - scoped(::User.system, "127.0.0.1", &block) - else - self.user = User.system - self.ip_addr = "127.0.0.1" - end - end - def self.user RequestStore[:current_user] end diff --git a/app/logical/d_text.rb b/app/logical/d_text.rb index 9d37d74ec..26f451a47 100644 --- a/app/logical/d_text.rb +++ b/app/logical/d_text.rb @@ -11,7 +11,7 @@ class DText html = DTextRagel.parse(text, **options) html = postprocess(html, *data) html - rescue DTextRagel::Error => e + rescue DTextRagel::Error "" end @@ -135,7 +135,7 @@ class DText fragment = Nokogiri::HTML.fragment(html) titles = fragment.css("a.dtext-wiki-link").map do |node| - title = node["href"][%r!\A/wiki_pages/(.*)\z!i, 1] + title = node["href"][%r{\A/wiki_pages/(.*)\z}i, 1] title = CGI.unescape(title) title = WikiPage.normalize_title(title) title @@ -163,7 +163,7 @@ class DText string = string.dup string.gsub!(/\s*\[#{tag}\](?!\])\s*/mi, "\n\n[#{tag}]\n\n") - string.gsub!(/\s*\[\/#{tag}\]\s*/mi, "\n\n[/#{tag}]\n\n") + string.gsub!(%r{\s*\[/#{tag}\]\s*}mi, "\n\n[/#{tag}]\n\n") string.gsub!(/(?:\r?\n){3,}/, "\n\n") string.strip! @@ -203,7 +203,7 @@ class DText end end - text = text.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "") + text.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "") end def self.from_html(text, inline: false, &block) diff --git a/app/logical/danbooru/http.rb b/app/logical/danbooru/http.rb index 6e0b21711..6147218f7 100644 --- a/app/logical/danbooru/http.rb +++ b/app/logical/danbooru/http.rb @@ -1,17 +1,49 @@ +require "danbooru/http/html_adapter" +require "danbooru/http/xml_adapter" +require "danbooru/http/cache" +require "danbooru/http/redirector" +require "danbooru/http/retriable" +require "danbooru/http/session" +require "danbooru/http/spoof_referrer" +require "danbooru/http/unpolish_cloudflare" + module Danbooru class Http - DEFAULT_TIMEOUT = 3 + class DownloadError < StandardError; end + class FileTooLargeError < StandardError; end - attr_writer :cache, :http + DEFAULT_TIMEOUT = 10 + MAX_REDIRECTS = 5 + + attr_accessor :max_size, :http class << self - delegate :get, :post, :delete, :cache, :auth, :basic_auth, :headers, to: :new + delegate :get, :head, :put, :post, :delete, :cache, :follow, :max_size, :timeout, :auth, :basic_auth, :headers, :cookies, :use, :public_only, :download_media, to: :new + end + + def initialize + @http ||= + ::Danbooru::Http::ApplicationClient.new + .timeout(DEFAULT_TIMEOUT) + .headers("Accept-Encoding" => "gzip") + .headers("User-Agent": "#{Danbooru.config.canonical_app_name}/#{Rails.application.config.x.git_hash}") + .use(:auto_inflate) + .use(redirector: { max_redirects: MAX_REDIRECTS }) + .use(:session) end def get(url, **options) request(:get, url, **options) end + def head(url, **options) + request(:head, url, **options) + end + + def put(url, **options) + request(:get, url, **options) + end + def post(url, **options) request(:post, url, **options) end @@ -20,8 +52,16 @@ module Danbooru request(:delete, url, **options) end - def cache(expiry) - dup.tap { |o| o.cache = expiry.to_i } + def follow(*args) + dup.tap { |o| o.http = o.http.follow(*args) } + end + + def max_size(size) + dup.tap { |o| o.max_size = size } + end + + def timeout(*args) + dup.tap { |o| o.http = o.http.timeout(*args) } end def auth(*args) @@ -36,36 +76,66 @@ module Danbooru dup.tap { |o| o.http = o.http.headers(*args) } end + def cookies(*args) + dup.tap { |o| o.http = o.http.cookies(*args) } + end + + def use(*args) + dup.tap { |o| o.http = o.http.use(*args) } + end + + def cache(expires_in) + use(cache: { expires_in: expires_in }) + end + + # allow requests only to public IPs, not to local or private networks. + def public_only + dup.tap do |o| + o.http = o.http.dup.tap do |http| + http.default_options = http.default_options.with_socket_class(ValidatingSocket) + end + end + end + + concerning :DownloadMethods do + def download_media(url, file: Tempfile.new("danbooru-download-", binmode: true)) + response = get(url) + + raise DownloadError, "Downloading #{response.uri} failed with code #{response.status}" if response.status != 200 + raise FileTooLargeError, response if @max_size && response.content_length.to_i > @max_size + + size = 0 + response.body.each do |chunk| + size += chunk.size + raise FileTooLargeError if @max_size && size > @max_size + file.write(chunk) + end + + file.rewind + [response, MediaFile.open(file)] + end + end + protected def request(method, url, **options) - if @cache.present? - cached_request(method, url, **options) - else - raw_request(method, url, **options) - end - rescue HTTP::TimeoutError - # return a synthetic http error on connection timeouts - ::HTTP::Response.new(status: 522, body: "", version: "1.1") - end - - def cached_request(method, url, **options) - key = Cache.hash({ method: method, url: url, headers: http.default_options.headers.to_h, **options }.to_json) - - cached_response = Cache.get(key, @cache) do - response = raw_request(method, url, **options) - { status: response.status, body: response.to_s, headers: response.headers.to_h, version: "1.1" } - end - - ::HTTP::Response.new(**cached_response) - end - - def raw_request(method, url, **options) http.send(method, url, **options) + rescue OpenSSL::SSL::SSLError + fake_response(590, "") + rescue ValidatingSocket::ProhibitedIpError + fake_response(591, "") + rescue HTTP::Redirector::TooManyRedirectsError + fake_response(596, "") + rescue HTTP::TimeoutError + fake_response(597, "") + rescue HTTP::ConnectionError + fake_response(598, "") + rescue HTTP::Error + fake_response(599, "") end - def http - @http ||= ::HTTP.timeout(DEFAULT_TIMEOUT).use(:auto_inflate).headers(Danbooru.config.http_headers).headers("Accept-Encoding" => "gzip") + def fake_response(status, body) + ::HTTP::Response.new(status: status, version: "1.1", body: ::HTTP::Response::Body.new(body)) end end end diff --git a/app/logical/danbooru/http/application_client.rb b/app/logical/danbooru/http/application_client.rb new file mode 100644 index 000000000..27ac6a6d1 --- /dev/null +++ b/app/logical/danbooru/http/application_client.rb @@ -0,0 +1,31 @@ +# An extension to HTTP::Client that lets us write Rack-style middlewares that +# hook into the request/response cycle and override how requests are made. This +# works by extending http.rb's concept of features (HTTP::Feature) to give them +# a `perform` method that takes a http request and returns a http response. +# This can be used to intercept and modify requests and return arbitrary responses. + +module Danbooru + class Http + class ApplicationClient < HTTP::Client + # Override `perform` to call the `perform` method on features first. + def perform(request, options) + features = options.features.values.reverse.select do |feature| + feature.respond_to?(:perform) + end + + perform = proc { |req| super(req, options) } + callback_chain = features.reduce(perform) do |callback_chain, feature| + proc { |req| feature.perform(req, &callback_chain) } + end + + callback_chain.call(request) + end + + # Override `branch` to return an ApplicationClient instead of a + # HTTP::Client so that chaining works. + def branch(...) + ApplicationClient.new(...) + end + end + end +end diff --git a/app/logical/danbooru/http/cache.rb b/app/logical/danbooru/http/cache.rb new file mode 100644 index 000000000..43932bd28 --- /dev/null +++ b/app/logical/danbooru/http/cache.rb @@ -0,0 +1,30 @@ +module Danbooru + class Http + class Cache < HTTP::Feature + HTTP::Options.register_feature :cache, self + + attr_reader :expires_in + + def initialize(expires_in:) + @expires_in = expires_in + end + + def perform(request, &block) + ::Cache.get(cache_key(request), expires_in) do + response = yield request + + # XXX hack to remove connection state from response body so we can serialize it for caching. + response.flush + response.body.instance_variable_set(:@connection, nil) + response.body.instance_variable_set(:@stream, nil) + + response + end + end + + def cache_key(request) + "http:" + ::Cache.hash({ method: request.verb, url: request.uri.to_s, headers: request.headers.sort }.to_json) + end + end + end +end diff --git a/app/logical/danbooru/http/html_adapter.rb b/app/logical/danbooru/http/html_adapter.rb new file mode 100644 index 000000000..733208e17 --- /dev/null +++ b/app/logical/danbooru/http/html_adapter.rb @@ -0,0 +1,12 @@ +module Danbooru + class Http + class HtmlAdapter < HTTP::MimeType::Adapter + HTTP::MimeType.register_adapter "text/html", self + HTTP::MimeType.register_alias "text/html", :html + + def decode(str) + Nokogiri::HTML5(str) + end + end + end +end diff --git a/app/logical/danbooru/http/redirector.rb b/app/logical/danbooru/http/redirector.rb new file mode 100644 index 000000000..26331226e --- /dev/null +++ b/app/logical/danbooru/http/redirector.rb @@ -0,0 +1,40 @@ +# A HTTP::Feature that automatically follows HTTP redirects. +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections + +module Danbooru + class Http + class Redirector < HTTP::Feature + HTTP::Options.register_feature :redirector, self + + attr_reader :max_redirects + + def initialize(max_redirects: 5) + @max_redirects = max_redirects + end + + def perform(request, &block) + response = yield request + + redirects = max_redirects + while response.status.redirect? + raise HTTP::Redirector::TooManyRedirectsError if redirects <= 0 + + response = yield build_redirect(request, response) + redirects -= 1 + end + + response + end + + def build_redirect(request, response) + location = response.headers["Location"].to_s + uri = HTTP::URI.parse(location) + + verb = request.verb + verb = :get if response.status == 303 && !request.verb.in?([:get, :head]) + + request.redirect(uri, verb) + end + end + end +end diff --git a/app/logical/danbooru/http/retriable.rb b/app/logical/danbooru/http/retriable.rb new file mode 100644 index 000000000..23d5e865a --- /dev/null +++ b/app/logical/danbooru/http/retriable.rb @@ -0,0 +1,54 @@ +# A HTTP::Feature that automatically retries requests that return a 429 error +# or a Retry-After header. Usage: `Danbooru::Http.use(:retriable).get(url)`. +# +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + +module Danbooru + class Http + class Retriable < HTTP::Feature + HTTP::Options.register_feature :retriable, self + + attr_reader :max_retries, :max_delay + + def initialize(max_retries: 2, max_delay: 5.seconds) + @max_retries = max_retries + @max_delay = max_delay + end + + def perform(request, &block) + response = yield request + + retries = max_retries + while retriable?(response) && retries > 0 && retry_delay(response) <= max_delay + DanbooruLogger.info "Retrying url=#{request.uri} status=#{response.status} retries=#{retries} delay=#{retry_delay(response)}" + + retries -= 1 + sleep(retry_delay(response)) + response = yield request + end + + response + end + + def retriable?(response) + response.status == 429 || response.headers["Retry-After"].present? + end + + def retry_delay(response, current_time: Time.zone.now) + retry_after = response.headers["Retry-After"] + + if retry_after.blank? + 0.seconds + elsif retry_after =~ /\A\d+\z/ + retry_after.to_i.seconds + else + retry_at = Time.zone.parse(retry_after) + return 0.seconds if retry_at.blank? + + [retry_at - current_time, 0].max.seconds + end + end + end + end +end diff --git a/app/logical/danbooru/http/session.rb b/app/logical/danbooru/http/session.rb new file mode 100644 index 000000000..3bb532928 --- /dev/null +++ b/app/logical/danbooru/http/session.rb @@ -0,0 +1,37 @@ +module Danbooru + class Http + class Session < HTTP::Feature + HTTP::Options.register_feature :session, self + + attr_reader :cookie_jar + + def initialize(cookie_jar: HTTP::CookieJar.new) + @cookie_jar = cookie_jar + end + + def perform(request) + add_cookies(request) + response = yield request + save_cookies(response) + response + end + + def add_cookies(request) + cookies = cookies_for_request(request) + request.headers["Cookie"] = cookies if cookies.present? + end + + def cookies_for_request(request) + saved_cookies = cookie_jar.each(request.uri).map { |c| [c.name, c.value] }.to_h + request_cookies = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"].to_s) + saved_cookies.merge(request_cookies).map { |name, value| "#{name}=#{value}" }.join("; ") + end + + def save_cookies(response) + response.cookies.each do |cookie| + cookie_jar.add(cookie) + end + end + end + end +end diff --git a/app/logical/danbooru/http/spoof_referrer.rb b/app/logical/danbooru/http/spoof_referrer.rb new file mode 100644 index 000000000..7df6faef9 --- /dev/null +++ b/app/logical/danbooru/http/spoof_referrer.rb @@ -0,0 +1,13 @@ +module Danbooru + class Http + class SpoofReferrer < HTTP::Feature + HTTP::Options.register_feature :spoof_referrer, self + + def perform(request, &block) + request.headers["Referer"] = request.uri.origin unless request.headers["Referer"].present? + response = yield request + response + end + end + end +end diff --git a/app/logical/danbooru/http/unpolish_cloudflare.rb b/app/logical/danbooru/http/unpolish_cloudflare.rb new file mode 100644 index 000000000..5ad62dcba --- /dev/null +++ b/app/logical/danbooru/http/unpolish_cloudflare.rb @@ -0,0 +1,20 @@ +# Bypass Cloudflare Polish (https://support.cloudflare.com/hc/en-us/articles/360000607372-Using-Cloudflare-Polish-to-compress-images) + +module Danbooru + class Http + class UnpolishCloudflare < HTTP::Feature + HTTP::Options.register_feature :unpolish_cloudflare, self + + def perform(request, &block) + response = yield request + + if response.headers["CF-Polished"].present? + request.uri.query_values = request.uri.query_values.to_h.merge(danbooru_no_polish: SecureRandom.uuid) + response = yield request + end + + response + end + end + end +end diff --git a/app/logical/danbooru/http/xml_adapter.rb b/app/logical/danbooru/http/xml_adapter.rb new file mode 100644 index 000000000..70c901c07 --- /dev/null +++ b/app/logical/danbooru/http/xml_adapter.rb @@ -0,0 +1,12 @@ +module Danbooru + class Http + class XmlAdapter < HTTP::MimeType::Adapter + HTTP::MimeType.register_adapter "application/xml", self + HTTP::MimeType.register_alias "application/xml", :xml + + def decode(str) + Hash.from_xml(str).with_indifferent_access + end + end + end +end diff --git a/app/logical/danbooru_maintenance.rb b/app/logical/danbooru_maintenance.rb index 330ba8c0f..e12a12f51 100644 --- a/app/logical/danbooru_maintenance.rb +++ b/app/logical/danbooru_maintenance.rb @@ -2,14 +2,13 @@ module DanbooruMaintenance module_function def hourly + safely { Upload.prune! } end def daily safely { PostPruner.new.prune! } - safely { Upload.prune! } safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all } safely { PostDisapproval.prune! } - safely { PostDisapproval.dmail_messages! } safely { regenerate_post_counts! } safely { TokenBucket.prune! } safely { BulkUpdateRequestPruner.warn_old } diff --git a/app/logical/downloads/file.rb b/app/logical/downloads/file.rb deleted file mode 100644 index 8bbab504b..000000000 --- a/app/logical/downloads/file.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'resolv' - -module Downloads - class File - include ActiveModel::Validations - class Error < StandardError; end - - RETRIABLE_ERRORS = [Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EIO, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Timeout::Error, IOError] - - delegate :data, to: :strategy - attr_reader :url, :referer - - validate :validate_url - - def initialize(url, referer = nil) - @url = Addressable::URI.parse(url) rescue nil - @referer = referer - validate! - end - - def size - res = HTTParty.head(uncached_url, **httparty_options, timeout: 3) - - if res.success? - res.content_length - else - raise HTTParty::ResponseError.new(res) - end - end - - def download!(url: uncached_url, tries: 3, **options) - Retriable.retriable(on: RETRIABLE_ERRORS, tries: tries, base_interval: 0) do - file = http_get_streaming(url, headers: strategy.headers, **options) - return [file, strategy] - end - end - - def validate_url - errors[:base] << "URL must not be blank" if url.blank? - errors[:base] << "'#{url}' is not a valid url" if !url.host.present? - errors[:base] << "'#{url}' is not a valid url. Did you mean 'http://#{url}'?" if !url.scheme.in?(%w[http https]) - end - - def http_get_streaming(url, file: Tempfile.new(binmode: true), headers: {}, max_size: Danbooru.config.max_file_size) - size = 0 - - res = HTTParty.get(url, httparty_options) do |chunk| - next if chunk.code == 302 - - size += chunk.size - raise Error.new("File is too large (max size: #{max_size})") if size > max_size && max_size > 0 - - file.write(chunk) - end - - if res.success? - file.rewind - return file - else - raise Error.new("HTTP error code: #{res.code} #{res.message}") - end - end - - # Prevent Cloudflare from potentially mangling the image. See issue #3528. - def uncached_url - return file_url unless is_cloudflare?(file_url) - - url = file_url.dup - url.query_values = url.query_values.to_h.merge(danbooru_no_cache: SecureRandom.uuid) - url - end - - def preview_url - @preview_url ||= Addressable::URI.parse(strategy.preview_url) - end - - def file_url - @file_url ||= Addressable::URI.parse(strategy.image_url) - end - - def strategy - @strategy ||= Sources::Strategies.find(url.to_s, referer) - end - - def httparty_options - { - timeout: 10, - stream_body: true, - headers: strategy.headers, - connection_adapter: ValidatingConnectionAdapter - }.deep_merge(Danbooru.config.httparty_options) - end - - def is_cloudflare?(url) - ip_addr = IPAddr.new(Resolv.getaddress(url.hostname)) - CloudflareService.new.ips.any? { |subnet| subnet.include?(ip_addr) } - end - - def self.banned_ip?(ip) - ip = IPAddress.parse(ip.to_s) unless ip.is_a?(IPAddress) - - if ip.ipv4? - ip.loopback? || ip.link_local? || ip.multicast? || ip.private? - elsif ip.ipv6? - ip.loopback? || ip.link_local? || ip.unique_local? || ip.unspecified? - end - end - end - - # Hook into HTTParty to validate the IP before following redirects. - # https://www.rubydoc.info/github/jnunemaker/httparty/HTTParty/ConnectionAdapter - class ValidatingConnectionAdapter < HTTParty::ConnectionAdapter - def self.call(uri, options) - ip_addr = IPAddress.parse(::Resolv.getaddress(uri.hostname)) - - if Downloads::File.banned_ip?(ip_addr) - raise Downloads::File::Error, "Downloads from #{ip_addr} are not allowed" - end - - super(uri, options) - end - end -end diff --git a/app/logical/dtext_input.rb b/app/logical/dtext_input.rb new file mode 100644 index 000000000..a1910e933 --- /dev/null +++ b/app/logical/dtext_input.rb @@ -0,0 +1,30 @@ +# A custom SimpleForm input for DText fields. +# +# Usage: +# +# <%= f.input :body, as: :dtext %> +# <%= f.input :reason, as: :dtext, inline: true %> +# +# https://github.com/heartcombo/simple_form/wiki/Custom-inputs-examples +# https://github.com/heartcombo/simple_form/blob/master/lib/simple_form/inputs/string_input.rb +# https://github.com/heartcombo/simple_form/blob/master/lib/simple_form/inputs/text_input.rb + +class DtextInput < SimpleForm::Inputs::Base + enable :placeholder, :maxlength, :minlength + + def input(wrapper_options) + t = template + merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) + + t.tag.div(class: "dtext-previewable") do + if options[:inline] + t.concat @builder.text_field(attribute_name, merged_input_options) + else + t.concat @builder.text_area(attribute_name, { rows: 20, cols: 30 }.merge(merged_input_options)) + end + + t.concat t.tag.div(id: "dtext-preview", class: "dtext-preview prose") + t.concat t.tag.span(t.link_to("Formatting help", t.dtext_help_path, remote: true, method: :get), class: "hint dtext-hint") + end + end +end diff --git a/app/logical/image_proxy.rb b/app/logical/image_proxy.rb index f10fd53c4..597d1210f 100644 --- a/app/logical/image_proxy.rb +++ b/app/logical/image_proxy.rb @@ -1,4 +1,6 @@ class ImageProxy + class Error < StandardError; end + def self.needs_proxy?(url) fake_referer_for(url).present? end @@ -8,19 +10,13 @@ class ImageProxy end def self.get_image(url) - if url.blank? - raise "Must specify url" - end + raise Error, "URL not present" unless url.present? + raise Error, "Proxy not allowed for this url (url=#{url})" unless needs_proxy?(url) - if !needs_proxy?(url) - raise "Proxy not allowed for this site" - end + referer = fake_referer_for(url) + response = Danbooru::Http.timeout(30).headers(Referer: referer).get(url) + raise Error, "Couldn't proxy image (code=#{response.status}, url=#{url})" unless response.status.success? - response = HTTParty.get(url, Danbooru.config.httparty_options.deep_merge(headers: {"Referer" => fake_referer_for(url)})) - if response.success? - return response - else - raise "HTTP error code: #{response.code} #{response.message}" - end + response end end diff --git a/app/logical/ip_lookup.rb b/app/logical/ip_lookup.rb index dc9f5bfce..7a6e3bf1a 100644 --- a/app/logical/ip_lookup.rb +++ b/app/logical/ip_lookup.rb @@ -16,7 +16,7 @@ class IpLookup end def info - return {} unless api_key.present? + return {} if api_key.blank? response = Danbooru::Http.cache(cache_duration).get("https://api.ipregistry.co/#{ip_addr}?key=#{api_key}") return {} if response.status != 200 json = response.parse.deep_symbolize_keys.with_indifferent_access diff --git a/app/logical/iqdb_proxy.rb b/app/logical/iqdb_proxy.rb index a812596ff..41dc5cd63 100644 --- a/app/logical/iqdb_proxy.rb +++ b/app/logical/iqdb_proxy.rb @@ -1,19 +1,24 @@ class IqdbProxy class Error < StandardError; end + attr_reader :http, :iqdbs_server - def self.enabled? - Danbooru.config.iqdbs_server.present? + def initialize(http: Danbooru::Http.new, iqdbs_server: Danbooru.config.iqdbs_server) + @iqdbs_server = iqdbs_server + @http = http end - def self.download(url, type) - download = Downloads::File.new(url) - file, strategy = download.download!(url: download.send(type)) + def enabled? + iqdbs_server.present? + end + + def download(url, type) + strategy = Sources::Strategies.find(url) + download_url = strategy.send(type) + file = strategy.download_file!(download_url) file end - def self.search(params) - raise NotImplementedError, "the IQDBs service isn't configured" unless enabled? - + def search(params) limit = params[:limit]&.to_i&.clamp(1, 1000) || 20 similarity = params[:similarity]&.to_f&.clamp(0.0, 100.0) || 0.0 high_similarity = params[:high_similarity]&.to_f&.clamp(0.0, 100.0) || 65.0 @@ -28,7 +33,7 @@ class IqdbProxy file = download(params[:image_url], :url) results = query(file: file, limit: limit) elsif params[:file_url].present? - file = download(params[:file_url], :file_url) + file = download(params[:file_url], :image_url) results = query(file: file, limit: limit) elsif params[:post_id].present? url = Post.find(params[:post_id]).preview_file_url @@ -46,15 +51,21 @@ class IqdbProxy file.try(:close) end - def self.query(params) - response = HTTParty.post("#{Danbooru.config.iqdbs_server}/similar", body: params, **Danbooru.config.httparty_options) - raise Error, "IQDB error: #{response.code} #{response.message}" unless response.success? - raise Error, "IQDB error: #{response.parsed_response["error"]}" if response.parsed_response.is_a?(Hash) - raise Error, "IQDB error: #{response.parsed_response.first}" if response.parsed_response.try(:first).is_a?(String) - response.parsed_response + def query(file: nil, url: nil, limit: 20) + raise NotImplementedError, "the IQDBs service isn't configured" unless enabled? + + file = HTTP::FormData::File.new(file) if file + form = { file: file, url: url, limit: limit }.compact + response = http.timeout(30).post("#{iqdbs_server}/similar", form: form) + + raise Error, "IQDB error: #{response.status}" if response.status != 200 + raise Error, "IQDB error: #{response.parse["error"]}" if response.parse.is_a?(Hash) + raise Error, "IQDB error: #{response.parse.first}" if response.parse.try(:first).is_a?(String) + + response.parse end - def self.decorate_posts(json) + def decorate_posts(json) post_ids = json.map { |match| match["post_id"] } posts = Post.where(id: post_ids).group_by(&:id).transform_values(&:first) diff --git a/app/logical/media_file.rb b/app/logical/media_file.rb index c28bf25a9..334de8f05 100644 --- a/app/logical/media_file.rb +++ b/app/logical/media_file.rb @@ -43,6 +43,8 @@ class MediaFile else :bin end + rescue EOFError + :bin end def self.videos_enabled? diff --git a/app/logical/media_file/flash.rb b/app/logical/media_file/flash.rb index 6ff99c239..b3d3815ae 100644 --- a/app/logical/media_file/flash.rb +++ b/app/logical/media_file/flash.rb @@ -42,7 +42,7 @@ class MediaFile::Flash < MediaFile signature = contents[0..2] # SWF version - version = contents[3].unpack('C').join.to_i + _version = contents[3].unpack('C').join.to_i # Determine the length of the uncompressed stream length = contents[4..7].unpack('V').join.to_i @@ -50,7 +50,7 @@ class MediaFile::Flash < MediaFile # If we do, in fact, have compression if signature == 'CWS' # Decompress the body of the SWF - body = Zlib::Inflate.inflate( contents[8..length] ) + body = Zlib::Inflate.inflate(contents[8..length]) # And reconstruct the stream contents to the first 8 bytes (header) # Plus our decompressed body @@ -58,10 +58,10 @@ class MediaFile::Flash < MediaFile end # Determine the nbits of our dimensions rectangle - nbits = contents.unpack('C'*contents.length)[8] >> 3 + nbits = contents.unpack('C' * contents.length)[8] >> 3 # Determine how many bits long this entire RECT structure is - rectbits = 5 + nbits * 4 # 5 bits for nbits, as well as nbits * number of fields (4) + rectbits = 5 + nbits * 4 # 5 bits for nbits, as well as nbits * number of fields (4) # Determine how many bytes rectbits composes (ceil(rectbits/8)) rectbytes = (rectbits.to_f / 8).ceil @@ -70,11 +70,11 @@ class MediaFile::Flash < MediaFile rect = contents[8..(8 + rectbytes)].unpack("#{'B8' * rectbytes}").join # Read in nbits incremenets starting from 5 - dimensions = Array.new + dimensions = [] 4.times do |n| s = 5 + (n * nbits) # Calculate our start index e = s + (nbits - 1) # Calculate our end index - dimensions[n] = rect[s..e].to_i(2) # Read that range (binary) and convert it to an integer + dimensions[n] = rect[s..e].to_i(2) # Read that range (binary) and convert it to an integer end # The values we have here are in "twips" diff --git a/app/logical/missed_search_service.rb b/app/logical/missed_search_service.rb deleted file mode 100644 index f98e3d395..000000000 --- a/app/logical/missed_search_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -# queries reportbooru to find missed post searches -class MissedSearchService - def self.enabled? - Danbooru.config.reportbooru_server.present? - end - - def initialize - if !MissedSearchService.enabled? - raise NotImplementedError.new("the Reportbooru service isn't configured. Missed searches are not available.") - end - end - - def each_search(&block) - fetch_data.scan(/(.+?) (\d+)\.0\n/).each(&block) - end - - def fetch_data - Cache.get("ms", 1.minute) do - url = URI.parse("#{Danbooru.config.reportbooru_server}/missed_searches") - response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6)) - if response.success? - response = response.body - else - response = "" - end - response.force_encoding("utf-8") - end - end -end diff --git a/app/logical/nico_seiga_api_client.rb b/app/logical/nico_seiga_api_client.rb index cb596a34e..4cdb75c9c 100644 --- a/app/logical/nico_seiga_api_client.rb +++ b/app/logical/nico_seiga_api_client.rb @@ -1,83 +1,102 @@ class NicoSeigaApiClient extend Memoist - BASE_URL = "http://seiga.nicovideo.jp/api" - attr_reader :illust_id + XML_API = "https://seiga.nicovideo.jp/api" - def self.agent - mech = Mechanize.new - mech.redirect_ok = false - mech.keep_alive = false + attr_reader :http - session = Cache.get("nico-seiga-session") - if session - cookie = Mechanize::Cookie.new("user_session", session) - cookie.domain = ".nicovideo.jp" - cookie.path = "/" - mech.cookie_jar.add(cookie) - else - mech.get("https://account.nicovideo.jp/login") do |page| - page.form_with(:id => "login_form") do |form| - form["mail_tel"] = Danbooru.config.nico_seiga_login - form["password"] = Danbooru.config.nico_seiga_password - end.click_button - end - session = mech.cookie_jar.cookies.select {|c| c.name == "user_session"}.first - if session - Cache.put("nico-seiga-session", session.value, 1.week) - else - raise "Session not found" + def initialize(work_id:, type:, http: Danbooru::Http.new) + @work_id = work_id + @work_type = type + @http = http + end + + def image_ids + if @work_type == "illust" + [api_response["id"]] + elsif @work_type == "manga" + manga_api_response.map do |x| + case x["meta"]["source_url"] + when %r{/thumb/(\d+)\w}i then Regexp.last_match(1) + when %r{nicoseiga\.cdn\.nimg\.jp/drm/image/\w+/(\d+)\w}i then Regexp.last_match(1) + end end end - - # This cookie needs to be set to allow viewing of adult works - cookie = Mechanize::Cookie.new("skip_fetish_warning", "1") - cookie.domain = "seiga.nicovideo.jp" - cookie.path = "/" - mech.cookie_jar.add(cookie) - - mech.redirect_ok = true - mech - end - - def initialize(illust_id:, user_id: nil) - @illust_id = illust_id - @user_id = user_id - end - - def image_id - illust_xml["response"]["image"]["id"].to_i - end - - def user_id - @user_id || illust_xml["response"]["image"]["user_id"].to_i end def title - illust_xml["response"]["image"]["title"] + api_response["title"] end - def desc - illust_xml["response"]["image"]["description"] + def description + api_response["description"] end - def moniker - artist_xml["response"]["user"]["nickname"] + def tags + api_response.dig("tag_list", "tag").to_a.map { |t| t["name"] }.compact end - def illust_xml - get("#{BASE_URL}/illust/info?id=#{illust_id}") + def user_id + api_response["user_id"] end - def artist_xml - get("#{BASE_URL}/user/info?id=#{user_id}") + def user_name + if @work_type == "illust" + api_response["nickname"] + elsif @work_type == "manga" + user_api_response(user_id)["nickname"] + end + end + + def api_response + if @work_type == "illust" + resp = get("https://sp.seiga.nicovideo.jp/ajax/seiga/im#{@work_id}") + return {} if resp.blank? || resp.code.to_i == 404 + api_response = JSON.parse(resp)["target_image"] + + elsif @work_type == "manga" + resp = http.cache(1.minute).get("#{XML_API}/theme/info?id=#{@work_id}") + return {} if resp.blank? || resp.code.to_i == 404 + api_response = Hash.from_xml(resp.to_s)["response"]["theme"] + end + + api_response || {} + rescue JSON::ParserError + {} + end + + def manga_api_response + resp = get("https://ssl.seiga.nicovideo.jp/api/v1/app/manga/episodes/#{@work_id}/frames") + return {} if resp.blank? || resp.code.to_i == 404 + JSON.parse(resp)["data"]["result"] + rescue JSON::ParserError + {} + end + + def user_api_response(user_id) + resp = http.cache(1.minute).get("#{XML_API}/user/info?id=#{user_id}") + return {} if resp.blank? || resp.code.to_i == 404 + Hash.from_xml(resp.to_s)["response"]["user"] + end + + def login + form = { + mail_tel: Danbooru.config.nico_seiga_login, + password: Danbooru.config.nico_seiga_password + } + + # XXX should fail gracefully instead of raising exception + resp = http.cache(1.hour).post("https://account.nicovideo.jp/login/redirector?site=seiga", form: form) + raise RuntimeError, "NicoSeiga login failed (status=#{resp.status} url=#{url})" if resp.status != 200 + + http end def get(url) - response = Danbooru::Http.cache(1.minute).get(url) - raise "nico seiga api call failed (code=#{response.code}, body=#{response.body})" if response.code != 200 + resp = login.cache(1.minute).get(url) + #raise RuntimeError, "NicoSeiga get failed (status=#{resp.status} url=#{url})" if resp.status != 200 - Hash.from_xml(response.to_s) + resp end - memoize :artist_xml, :illust_xml + memoize :api_response, :manga_api_response, :user_api_response end diff --git a/app/logical/nico_seiga_manga_api_client.rb b/app/logical/nico_seiga_manga_api_client.rb deleted file mode 100644 index 4bdb6a7f4..000000000 --- a/app/logical/nico_seiga_manga_api_client.rb +++ /dev/null @@ -1,60 +0,0 @@ -class NicoSeigaMangaApiClient - extend Memoist - BASE_URL = "https://seiga.nicovideo.jp/api" - attr_reader :theme_id - - def initialize(theme_id) - @theme_id = theme_id - end - - def user_id - theme_info_xml["response"]["theme"]["user_id"].to_i - end - - def title - theme_info_xml["response"]["theme"]["title"] - end - - def desc - theme_info_xml["response"]["theme"]["description"] - end - - def moniker - artist_xml["response"]["user"]["nickname"] - end - - def image_ids - images = theme_data_xml["response"]["image_list"]["image"] - images = [images] unless images.is_a?(Array) - images.map {|x| x["id"]} - end - - def tags - theme_info_xml["response"]["theme"]["tag_list"]["tag"].map {|x| x["name"]} - end - - def theme_data_xml - uri = "#{BASE_URL}/theme/data?theme_id=#{theme_id}" - body = NicoSeigaApiClient.agent.get(uri).body - Hash.from_xml(body) - end - - def theme_info_xml - uri = "#{BASE_URL}/theme/info?id=#{theme_id}" - body = NicoSeigaApiClient.agent.get(uri).body - Hash.from_xml(body) - end - - def artist_xml - get("#{BASE_URL}/user/info?id=#{user_id}") - end - - def get(url) - response = Danbooru::Http.cache(1.minute).get(url) - raise "nico seiga api call failed (code=#{response.code}, body=#{response.body})" if response.code != 200 - - Hash.from_xml(response.to_s) - end - - memoize :theme_data_xml, :theme_info_xml, :artist_xml -end diff --git a/app/logical/pagination_extension.rb b/app/logical/pagination_extension.rb index 5d5113e8c..dce0f8ac5 100644 --- a/app/logical/pagination_extension.rb +++ b/app/logical/pagination_extension.rb @@ -3,9 +3,9 @@ module PaginationExtension attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode - def paginate(page, limit: nil, count: nil, search_count: nil) + def paginate(page, limit: nil, max_limit: 1000, count: nil, search_count: nil) @records_per_page = limit || Danbooru.config.posts_per_page - @records_per_page = @records_per_page.to_i.clamp(1, 1000) + @records_per_page = @records_per_page.to_i.clamp(1, max_limit) if count.present? @paginator_count = count @@ -61,27 +61,35 @@ module PaginationExtension end def prev_page - return nil if is_first_page? - - if paginator_mode == :numbered + if is_first_page? + nil + elsif paginator_mode == :numbered current_page - 1 - elsif paginator_mode == :sequential_before + elsif paginator_mode == :sequential_before && records.present? "a#{records.first.id}" - elsif paginator_mode == :sequential_after + elsif paginator_mode == :sequential_after && records.present? "b#{records.last.id}" + else + nil end + rescue ActiveRecord::QueryCanceled + nil end def next_page - return nil if is_last_page? - - if paginator_mode == :numbered + if is_last_page? + nil + elsif paginator_mode == :numbered current_page + 1 - elsif paginator_mode == :sequential_before + elsif paginator_mode == :sequential_before && records.present? "b#{records.last.id}" - elsif paginator_mode == :sequential_after + elsif paginator_mode == :sequential_after && records.present? "a#{records.first.id}" + else + nil end + rescue ActiveRecord::QueryCanceled + nil end # XXX Hack: in sequential pagination we fetch one more record than we @@ -106,10 +114,7 @@ module PaginationExtension def total_count @paginator_count ||= unscoped.from(except(:offset, :limit, :order).reorder(nil)).count rescue ActiveRecord::StatementInvalid => e - if e.to_s =~ /statement timeout/ - @paginator_count ||= 1_000_000 - else - raise - end + raise unless e.to_s =~ /statement timeout/ + @paginator_count ||= 1_000_000 end end diff --git a/app/logical/pawoo_api_client.rb b/app/logical/pawoo_api_client.rb index 20daf1fd7..f74d42e38 100644 --- a/app/logical/pawoo_api_client.rb +++ b/app/logical/pawoo_api_client.rb @@ -128,15 +128,15 @@ class PawooApiClient rescue data = {} end - return Account.new(data) + Account.new(data) end end private def fetch_access_token - raise MissingConfigurationError.new("missing pawoo client id") if Danbooru.config.pawoo_client_id.nil? - raise MissingConfigurationError.new("missing pawoo client secret") if Danbooru.config.pawoo_client_secret.nil? + raise MissingConfigurationError, "missing pawoo client id" if Danbooru.config.pawoo_client_id.nil? + raise MissingConfigurationError, "missing pawoo client secret" if Danbooru.config.pawoo_client_secret.nil? Cache.get("pawoo-token") do result = client.client_credentials.get_token diff --git a/app/logical/pixiv_api_client.rb b/app/logical/pixiv_api_client.rb index f1d55c981..22e6c9955 100644 --- a/app/logical/pixiv_api_client.rb +++ b/app/logical/pixiv_api_client.rb @@ -1,5 +1,3 @@ -require 'resolv-replace' - class PixivApiClient extend Memoist @@ -114,66 +112,13 @@ class PixivApiClient end end - class FanboxResponse - attr_reader :json - - def initialize(json) - @json = json - end - - def name - json["body"]["user"]["name"] - end - - def user_id - json["body"]["user"]["userId"] - end - - def moniker - "" - end - - def page_count - json["body"]["body"]["images"].size - end - - def artist_commentary_title - json["body"]["title"] - end - - def artist_commentary_desc - json["body"]["body"]["text"] - end - - def tags - [] - end - - def pages - if json["body"]["body"] - json["body"]["body"]["images"].map {|x| x["originalUrl"]} - else - [] - end - end - end - def work(illust_id) - headers = Danbooru.config.http_headers.merge( - "Referer" => "http://www.pixiv.net", - "Content-Type" => "application/x-www-form-urlencoded", - "Authorization" => "Bearer #{access_token}" - ) - params = { - "image_sizes" => "large", - "include_stats" => "true" - } - + params = { image_sizes: "large", include_stats: "true" } url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/works/#{illust_id.to_i}.json" - response = Danbooru::Http.cache(1.minute).headers(headers).get(url, params: params) + response = api_client.cache(1.minute).get(url, params: params) json = response.parse - if response.code == 200 + if response.status == 200 WorkResponse.new(json["response"][0]) elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/ raise BadIDError.new("Pixiv ##{illust_id} not found: work was deleted, made private, or ID is invalid.") @@ -184,32 +129,12 @@ class PixivApiClient raise Error.new("Pixiv API call failed (status=#{response.code} body=#{response.body})") end - def fanbox(fanbox_id) - url = "https://www.pixiv.net/ajax/fanbox/post?postId=#{fanbox_id.to_i}" - resp = agent.get(url) - json = JSON.parse(resp.body) - if resp.code == "200" - FanboxResponse.new(json) - elsif json["status"] == "failure" - raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})") - end - rescue JSON::ParserError - raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})") - end - def novel(novel_id) - headers = Danbooru.config.http_headers.merge( - "Referer" => "http://www.pixiv.net", - "Content-Type" => "application/x-www-form-urlencoded", - "Authorization" => "Bearer #{access_token}" - ) - url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/novels/#{novel_id.to_i}.json" - resp = HTTParty.get(url, Danbooru.config.httparty_options.deep_merge(headers: headers)) - body = resp.body.force_encoding("utf-8") - json = JSON.parse(body) + resp = api_client.cache(1.minute).get(url) + json = resp.parse - if resp.success? + if resp.status == 200 NovelResponse.new(json["response"][0]) elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/ raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})") @@ -219,42 +144,41 @@ class PixivApiClient end def access_token - Cache.get("pixiv-papi-access-token", 3000) do - access_token = nil + # truncate timestamp to 1-hour resolution so that it doesn't break caching. + client_time = Time.zone.now.utc.change(min: 0).rfc3339 + client_hash = Digest::MD5.hexdigest(client_time + CLIENT_HASH_SALT) - client_time = Time.now.rfc3339 - client_hash = Digest::MD5.hexdigest(client_time + CLIENT_HASH_SALT) + headers = { + "Referer": "http://www.pixiv.net", + "X-Client-Time": client_time, + "X-Client-Hash": client_hash + } - headers = { - "Referer": "http://www.pixiv.net", - "X-Client-Time": client_time, - "X-Client-Hash": client_hash - } - params = { - username: Danbooru.config.pixiv_login, - password: Danbooru.config.pixiv_password, - grant_type: "password", - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET - } - url = "https://oauth.secure.pixiv.net/auth/token" + params = { + username: Danbooru.config.pixiv_login, + password: Danbooru.config.pixiv_password, + grant_type: "password", + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET + } - resp = HTTParty.post(url, Danbooru.config.httparty_options.deep_merge(body: params, headers: headers)) - body = resp.body.force_encoding("utf-8") + resp = http.headers(headers).cache(1.hour).post("https://oauth.secure.pixiv.net/auth/token", form: params) + return nil unless resp.status == 200 - if resp.success? - json = JSON.parse(body) - access_token = json["response"]["access_token"] - else - raise Error.new("Pixiv API access token call failed (status=#{resp.code} body=#{body})") - end - - access_token - end + resp.parse.dig("response", "access_token") end - def agent - PixivWebAgent.build + def api_client + http.headers( + "Referer": "http://www.pixiv.net", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer #{access_token}" + ) end - memoize :agent + + def http + Danbooru::Http.new + end + + memoize :access_token, :api_client, :http end diff --git a/app/logical/pixiv_web_agent.rb b/app/logical/pixiv_web_agent.rb deleted file mode 100644 index f48a20c50..000000000 --- a/app/logical/pixiv_web_agent.rb +++ /dev/null @@ -1,74 +0,0 @@ -class PixivWebAgent - SESSION_CACHE_KEY = "pixiv-phpsessid" - COMIC_SESSION_CACHE_KEY = "pixiv-comicsessid" - SESSION_COOKIE_KEY = "PHPSESSID" - COMIC_SESSION_COOKIE_KEY = "_pixiv-comic_session" - - def self.phpsessid(agent) - agent.cookies.select { |cookie| cookie.name == SESSION_COOKIE_KEY }.first.try(:value) - end - - def self.build - mech = Mechanize.new - mech.keep_alive = false - - phpsessid = Cache.get(SESSION_CACHE_KEY) - comicsessid = Cache.get(COMIC_SESSION_CACHE_KEY) - - if phpsessid - cookie = Mechanize::Cookie.new(SESSION_COOKIE_KEY, phpsessid) - cookie.domain = ".pixiv.net" - cookie.path = "/" - mech.cookie_jar.add(cookie) - - if comicsessid - cookie = Mechanize::Cookie.new(COMIC_SESSION_COOKIE_KEY, comicsessid) - cookie.domain = ".pixiv.net" - cookie.path = "/" - mech.cookie_jar.add(cookie) - end - else - headers = { - "Origin" => "https://accounts.pixiv.net", - "Referer" => "https://accounts.pixiv.net/login?lang=en^source=pc&view_type=page&ref=wwwtop_accounts_index" - } - - params = { - pixiv_id: Danbooru.config.pixiv_login, - password: Danbooru.config.pixiv_password, - captcha: nil, - g_captcha_response: nil, - source: "pc", - post_key: nil - } - - mech.get("https://accounts.pixiv.net/login?lang=en&source=pc&view_type=page&ref=wwwtop_accounts_index") do |page| - json = page.search("input#init-config").first.attr("value") - if json =~ /pixivAccount\.postKey":"([a-f0-9]+)/ - params[:post_key] = $1 - end - end - - mech.post("https://accounts.pixiv.net/api/login?lang=en", params, headers) - if mech.current_page.body =~ /"error":false/ - cookie = mech.cookies.select {|x| x.name == SESSION_COOKIE_KEY}.first - if cookie - Cache.put(SESSION_CACHE_KEY, cookie.value, 1.week) - end - end - - begin - mech.get("https://comic.pixiv.net") do |page| - cookie = mech.cookies.select {|x| x.name == COMIC_SESSION_COOKIE_KEY}.first - if cookie - Cache.put(COMIC_SESSION_CACHE_KEY, cookie.value, 1.week) - end - end - rescue Net::HTTPServiceUnavailable - # ignore - end - end - - mech - end -end diff --git a/app/logical/popular_search_service.rb b/app/logical/popular_search_service.rb deleted file mode 100644 index 026684f3c..000000000 --- a/app/logical/popular_search_service.rb +++ /dev/null @@ -1,61 +0,0 @@ -# queries reportbooru to find popular post searches -class PopularSearchService - attr_reader :date - - def self.enabled? - Danbooru.config.reportbooru_server.present? - end - - def initialize(date) - if !PopularSearchService.enabled? - raise NotImplementedError.new("the Reportbooru service isn't configured. Popular searches are not available.") - end - - @date = date - end - - def each_search(limit = 100, &block) - JSON.parse(fetch_data.to_s).slice(0, limit).each(&block) - end - - def tags - JSON.parse(fetch_data.to_s).map {|x| x[0]} - end - - def fetch_data - return [] unless self.class.enabled? - - dates = date.strftime("%Y-%m-%d") - - data = Cache.get("ps-day-#{dates}", 1.minute) do - url = "#{Danbooru.config.reportbooru_server}/post_searches/rank?date=#{dates}" - response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 3)) - if response.success? - response = response.body - else - response = "[]" - end - response - end.to_s.force_encoding("utf-8") - - if data.blank? || data == "[]" - dates = date.yesterday.strftime("%Y-%m-%d") - - data = Cache.get("ps-day-#{dates}", 1.minute) do - url = "#{Danbooru.config.reportbooru_server}/post_searches/rank?date=#{dates}" - response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 3)) - if response.success? - response = response.body - else - response = "[]" - end - response - end.to_s.force_encoding("utf-8") - end - - data - rescue StandardError => e - DanbooruLogger.log(e) - return [] - end -end diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index 8d3b61d66..0829ce3ba 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -307,6 +307,8 @@ class PostQueryBuilder Post.where(parent: nil) when "any" Post.where.not(parent: nil) + when /pending|flagged|modqueue|deleted|banned|active|unmoderated/ + Post.where.not(parent: nil).where(parent: status_matches(parent)) when /\A\d+\z/ Post.where(id: parent).or(Post.where(parent: parent)) else @@ -320,6 +322,8 @@ class PostQueryBuilder Post.where(has_children: false) when "any" Post.where(has_children: true) + when /pending|flagged|modqueue|deleted|banned|active|unmoderated/ + Post.where(has_children: true).where(children: status_matches(child)) else Post.none end diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index e12315873..80052c527 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -24,9 +24,8 @@ module PostSets end def wiki_page - return nil unless tag.present? && tag.wiki_page.present? - return nil unless !tag.wiki_page.is_deleted? - tag.wiki_page + return nil unless normalized_query.has_single_tag? + @wiki_page ||= WikiPage.undeleted.find_by(title: normalized_query.tags.first.name) end def tag @@ -77,7 +76,11 @@ module PostSets end def per_page - (@per_page || query.find_metatag(:limit) || CurrentUser.user.per_page).to_i.clamp(0, MAX_PER_PAGE) + (@per_page || query.find_metatag(:limit) || CurrentUser.user.per_page).to_i.clamp(0, max_per_page) + end + + def max_per_page + (format == "sitemap") ? 10_000 : MAX_PER_PAGE end def is_random? @@ -94,7 +97,7 @@ module PostSets end def get_random_posts - per_page.times.inject([]) do |all, x| + per_page.times.inject([]) do |all, _| all << ::Post.user_tag_match(tag_string).random end.compact.uniq end @@ -104,15 +107,15 @@ module PostSets @post_count = get_post_count if is_random? - temp = get_random_posts + get_random_posts else - temp = normalized_query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page) + normalized_query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load end end end def hide_from_crawler? - return true if current_page > 1 + return true if current_page > 50 return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank) true end @@ -160,20 +163,16 @@ module PostSets elsif query.is_metatag?(:search) saved_search_tags elsif query.is_empty_search? || query.is_metatag?(:order, :rank) - popular_tags + popular_tags.presence || frequent_tags elsif query.is_single_term? - similar_tags + similar_tags.presence || frequent_tags else frequent_tags end end def popular_tags - if PopularSearchService.enabled? - PopularSearchService.new(Date.today).tags - else - frequent_tags - end + ReportbooruService.new.popular_searches(Date.today, limit: MAX_SIDEBAR_TAGS) end def similar_tags diff --git a/app/logical/post_view_count_service.rb b/app/logical/post_view_count_service.rb deleted file mode 100644 index 8564a027f..000000000 --- a/app/logical/post_view_count_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -class PostViewCountService - def self.enabled? - Danbooru.config.reportbooru_server.present? - end - - def initialize - if !PostViewCountService.enabled? - raise NotImplementedError.new("the Reportbooru service isn't configured. Post views are not available.") - end - end - - def fetch_count(post_id) - url = URI.parse("#{Danbooru.config.reportbooru_server}/post_views/#{post_id}") - response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6)) - if response.success? - return JSON.parse(response.body) - else - return nil - end - end - - def fetch_rank(date = Date.today) - url = URI.parse("#{Danbooru.config.reportbooru_server}/post_views/rank?date=#{date}") - response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6)) - if response.success? - return JSON.parse(response.body) - else - return nil - end - rescue JSON::ParserError - nil - end - - def popular_posts(date = Date.today) - ranking = fetch_rank(date) || [] - ranking.slice(0, 50).map {|x| Post.find(x[0])} - end -end diff --git a/app/logical/reportbooru_service.rb b/app/logical/reportbooru_service.rb new file mode 100644 index 000000000..898a67c98 --- /dev/null +++ b/app/logical/reportbooru_service.rb @@ -0,0 +1,50 @@ +class ReportbooruService + attr_reader :http, :reportbooru_server + + def initialize(http: Danbooru::Http.new, reportbooru_server: Danbooru.config.reportbooru_server) + @reportbooru_server = reportbooru_server + @http = http + end + + def enabled? + reportbooru_server.present? + end + + def missed_search_rankings(expires_in: 1.minutes) + return [] unless enabled? + + response = http.cache(expires_in).get("#{reportbooru_server}/missed_searches") + return [] if response.status != 200 + + body = response.to_s.force_encoding("utf-8") + body.lines.map(&:split).map { [_1, _2.to_i] } + end + + def post_search_rankings(date, expires_in: 1.minutes) + request("#{reportbooru_server}/post_searches/rank?date=#{date}", expires_in) + end + + def post_view_rankings(date, expires_in: 1.minutes) + request("#{reportbooru_server}/post_views/rank?date=#{date}", expires_in) + end + + def popular_searches(date, limit: 100) + ranking = post_search_rankings(date) + ranking = post_search_rankings(date.yesterday) if ranking.blank? + ranking.take(limit).map(&:first) + end + + def popular_posts(date, limit: 100) + ranking = post_view_rankings(date) + ranking = post_view_rankings(date.yesterday) if ranking.blank? + ranking.take(limit).map { |x| Post.find(x[0]) } + end + + def request(url, expires_in) + return [] unless enabled? + + response = http.cache(expires_in).get(url) + return [] if response.status != 200 + JSON.parse(response.to_s.force_encoding("utf-8")) + end +end diff --git a/app/logical/sources/strategies.rb b/app/logical/sources/strategies.rb index 0e2f2f82c..05a0ef2d5 100644 --- a/app/logical/sources/strategies.rb +++ b/app/logical/sources/strategies.rb @@ -1,7 +1,7 @@ module Sources module Strategies def self.all - return [ + [ Strategies::Pixiv, Strategies::NicoSeiga, Strategies::Twitter, @@ -13,7 +13,8 @@ module Sources Strategies::Pawoo, Strategies::Moebooru, Strategies::HentaiFoundry, - Strategies::Weibo + Strategies::Weibo, + Strategies::Newgrounds ] end diff --git a/app/logical/sources/strategies/art_station.rb b/app/logical/sources/strategies/art_station.rb index 2c78826fe..1c4d32b56 100644 --- a/app/logical/sources/strategies/art_station.rb +++ b/app/logical/sources/strategies/art_station.rb @@ -22,15 +22,15 @@ module Sources::Strategies class ArtStation < Base - PROJECT1 = %r!\Ahttps?://www\.artstation\.com/artwork/(?[a-z0-9-]+)/?\z!i - PROJECT2 = %r!\Ahttps?://(?[\w-]+)\.artstation\.com/projects/(?[a-z0-9-]+)(?:/|\?[\w=-]+)?\z!i + PROJECT1 = %r{\Ahttps?://www\.artstation\.com/artwork/(?[a-z0-9-]+)/?\z}i + PROJECT2 = %r{\Ahttps?://(?[\w-]+)\.artstation\.com/projects/(?[a-z0-9-]+)(?:/|\?[\w=-]+)?\z}i PROJECT = Regexp.union(PROJECT1, PROJECT2) ARTIST1 = %r{\Ahttps?://(?[\w-]+)(?[\w-]+)/?\z}i ARTIST3 = %r{\Ahttps?://www\.artstation\.com/(?[\w-]+)/?\z}i ARTIST = Regexp.union(ARTIST1, ARTIST2, ARTIST3) - ASSET = %r!\Ahttps?://cdn\w*\.artstation\.com/p/assets/(?images|covers)/images/(?\d+/\d+/\d+)/(?[^/]+)/(?.+)\z!i + ASSET = %r{\Ahttps?://cdn\w*\.artstation\.com/p/assets/(?images|covers)/images/(?\d+/\d+/\d+)/(?[^/]+)/(?.+)\z}i attr_reader :json @@ -144,10 +144,10 @@ module Sources::Strategies urls = image_url_sizes($~[:type], $~[:id], $~[:filename]) if size == :smallest - urls = urls.reverse() + urls = urls.reverse end - chosen_url = urls.find { |url| http_exists?(url, headers) } + chosen_url = urls.find { |url| http_exists?(url) } chosen_url || url end end diff --git a/app/logical/sources/strategies/base.rb b/app/logical/sources/strategies/base.rb index 55bf676ff..15f476a15 100644 --- a/app/logical/sources/strategies/base.rb +++ b/app/logical/sources/strategies/base.rb @@ -14,6 +14,8 @@ module Sources module Strategies class Base + class DownloadError < StandardError; end + attr_reader :url, :referer_url, :urls, :parsed_url, :parsed_referer, :parsed_urls extend Memoist @@ -35,9 +37,9 @@ module Sources # referrer_url so the strategy can discover the HTML # page and other information. def initialize(url, referer_url = nil) - @url = url - @referer_url = referer_url - @urls = [url, referer_url].select(&:present?) + @url = url.to_s + @referer_url = referer_url&.to_s + @urls = [@url, @referer_url].select(&:present?) @parsed_url = Addressable::URI.heuristic_parse(url) rescue nil @parsed_referer = Addressable::URI.heuristic_parse(referer_url) rescue nil @@ -58,8 +60,8 @@ module Sources end def site_name - Addressable::URI.heuristic_parse(url).host - rescue Addressable::URI::InvalidURIError => e + Addressable::URI.heuristic_parse(url)&.host + rescue Addressable::URI::InvalidURIError nil end @@ -90,9 +92,7 @@ module Sources # eventually be assigned as the source for the post, but it does not # represent what the downloader will fetch. def page_url - Rails.logger.warn "Valid page url for (#{url}, #{referer_url}) not found" - - return nil + nil end # This will be the url stored in posts. Typically this is the page @@ -141,14 +141,37 @@ module Sources # Subclasses should merge in any required headers needed to access resources # on the site. def headers - return Danbooru.config.http_headers + {} end # Returns the size of the image resource without actually downloading the file. - def size - Downloads::File.new(image_url).size + def remote_size + response = http_downloader.head(image_url) + return nil unless response.status == 200 && response.content_length.present? + + response.content_length.to_i end - memoize :size + memoize :remote_size + + # Download the file at the given url, or at the main image url by default. + def download_file!(download_url = image_url) + raise DownloadError, "Download failed: couldn't find download url for #{url}" if download_url.blank? + response, file = http_downloader.download_media(download_url) + raise DownloadError, "Download failed: #{download_url} returned error #{response.status}" if response.status != 200 + file + end + + # A http client for API requests. + def http + Danbooru::Http.new.public_only + end + memoize :http + + # A http client for downloading files. + def http_downloader + http.timeout(30).max_size(Danbooru.config.max_file_size).use(:spoof_referrer).use(:unpolish_cloudflare) + end + memoize :http_downloader # The url to use for artist finding purposes. This will be stored in the # artist entry. Normally this will be the profile url. @@ -189,7 +212,7 @@ module Sources end def normalized_tags - tags.map { |tag, url| normalize_tag(tag) }.sort.uniq + tags.map { |tag, _url| normalize_tag(tag) }.sort.uniq end def normalize_tag(tag) @@ -243,7 +266,7 @@ module Sources end def to_h - return { + { :artist => { :name => artist_name, :tag_name => tag_name, @@ -276,9 +299,8 @@ module Sources to_h.to_json end - def http_exists?(url, headers) - res = HTTParty.head(url, Danbooru.config.httparty_options.deep_merge(headers: headers)) - res.success? + def http_exists?(url) + http_downloader.head(url).status.success? end # Convert commentary to dtext by stripping html tags. Sites can override diff --git a/app/logical/sources/strategies/deviant_art.rb b/app/logical/sources/strategies/deviant_art.rb index 8bb193672..41c12382a 100644 --- a/app/logical/sources/strategies/deviant_art.rb +++ b/app/logical/sources/strategies/deviant_art.rb @@ -47,18 +47,18 @@ module Sources module Strategies class DeviantArt < Base - ASSET_SUBDOMAINS = %r{(?:fc|th|pre|img|orig|origin-orig)\d*}i + ASSET_SUBDOMAINS = /(?:fc|th|pre|img|orig|origin-orig)\d*/i RESERVED_SUBDOMAINS = %r{\Ahttps?://(?:#{ASSET_SUBDOMAINS}|www)\.}i MAIN_DOMAIN = %r{\Ahttps?://(?:www\.)?deviantart.com}i - TITLE = %r{(?[a-z0-9_-]+?)}i - ARTIST = %r{(?<artist>[a-z0-9_-]+?)}i - DEVIATION_ID = %r{(?<deviation_id>[0-9]+)}i + TITLE = /(?<title>[a-z0-9_-]+?)/i + ARTIST = /(?<artist>[a-z0-9_-]+?)/i + DEVIATION_ID = /(?<deviation_id>[0-9]+)/i - DA_FILENAME_1 = %r{[a-f0-9]{32}-d(?<base36_deviation_id>[a-z0-9]+)\.}i - DA_FILENAME_2 = %r{#{TITLE}(?:_by_#{ARTIST}(?:-d(?<base36_deviation_id>[a-z0-9]+))?)?\.}i + DA_FILENAME_1 = /[a-f0-9]{32}-d(?<base36_deviation_id>[a-z0-9]+)\./i + DA_FILENAME_2 = /#{TITLE}(?:_by_#{ARTIST}(?:-d(?<base36_deviation_id>[a-z0-9]+))?)?\./i DA_FILENAME = Regexp.union(DA_FILENAME_1, DA_FILENAME_2) - WIX_FILENAME = %r{d(?<base36_deviation_id>[a-z0-9]+)[0-9a-f-]+\.\w+(?:/\w+/\w+/[\w,]+/(?<title>[\w-]+)_by_(?<artist>[\w-]+)_d\w+-\w+\.\w+)?.+}i + WIX_FILENAME = %r{d(?<base36_deviation_id>[a-z0-9]+)[0-9a-f-]+\.\w+(?:/\w+/\w+/[\w,]+/(?<title>[\w-]+)_by_(?<artist>[\w-]+)_d\w+-\w+\.\w+)?.+}i NOT_NORMALIZABLE_ASSET = %r{\Ahttps?://#{ASSET_SUBDOMAINS}\.deviantart\.net/.+/[0-9a-f]{32}(?:-[^d]\w+)?\.}i @@ -75,7 +75,7 @@ module Sources PATH_PROFILE = %r{#{MAIN_DOMAIN}/#{ARTIST}/?\z}i SUBDOMAIN_PROFILE = %r{\Ahttps?://#{ARTIST}\.deviantart\.com/?\z}i - FAVME = %r{\Ahttps?://(www\.)?fav\.me/d(?<base36_deviation_id>[a-z0-9]+)\z}i + FAVME = %r{\Ahttps?://(?:www\.)?fav\.me/d(?<base36_deviation_id>[a-z0-9]+)\z}i def domains ["deviantart.net", "deviantart.com", "fav.me"] @@ -110,12 +110,12 @@ module Sources api_deviation[:videos].max_by { |x| x[:filesize] }[:src] else src = api_deviation.dig(:content, :src) - if deviation_id && deviation_id.to_i <= 790677560 && src =~ /^https:\/\/images-wixmp-/ && src !~ /\.gif\?/ - src = src.sub(%r!(/f/[a-f0-9-]+/[a-f0-9-]+)!, '/intermediary\1') - src = src.sub(%r!/v1/(fit|fill)/.*\z!i, "") + if deviation_id && deviation_id.to_i <= 790_677_560 && src =~ %r{\Ahttps://images-wixmp-} && src !~ /\.gif\?/ + src = src.sub(%r{(/f/[a-f0-9-]+/[a-f0-9-]+)}, '/intermediary\1') + src = src.sub(%r{/v1/(fit|fill)/.*\z}i, "") end - src = src.sub(%r!\Ahttps?://orig\d+\.deviantart\.net!i, "http://origin-orig.deviantart.net") - src = src.gsub(%r!q_\d+,strp!, "q_100") + src = src.sub(%r{\Ahttps?://orig\d+\.deviantart\.net}i, "http://origin-orig.deviantart.net") + src = src.gsub(/q_\d+,strp/, "q_100") src end end @@ -191,7 +191,7 @@ module Sources # <a href="https://sa-dui.deviantart.com/journal/About-Commissions-223178193" data-sigil="thumb" class="thumb lit" ...> if element["class"].split.include?("lit") - deviation_id = element["href"][%r!-(\d+)\z!, 1].to_i + deviation_id = element["href"][/-(\d+)\z/, 1].to_i element.content = "deviantart ##{deviation_id}" else element.content = "" @@ -199,7 +199,7 @@ module Sources end if element.name == "a" && element["href"].present? - element["href"] = element["href"].gsub(%r!\Ahttps?://www\.deviantart\.com/users/outgoing\?!i, "") + element["href"] = element["href"].gsub(%r{\Ahttps?://www\.deviantart\.com/users/outgoing\?}i, "") # href may be missing the `http://` bit (ex: `inprnt.com`, `//inprnt.com`). Add it if missing. uri = Addressable::URI.heuristic_parse(element["href"]) rescue nil @@ -283,7 +283,7 @@ module Sources return nil if meta.nil? appurl = meta["content"] - uuid = appurl[%r!\ADeviantArt://deviation/(.*)\z!, 1] + uuid = appurl[%r{\ADeviantArt://deviation/(.*)\z}, 1] uuid end memoize :uuid diff --git a/app/logical/sources/strategies/hentai_foundry.rb b/app/logical/sources/strategies/hentai_foundry.rb index 5ab3ca2ed..041fb8df2 100644 --- a/app/logical/sources/strategies/hentai_foundry.rb +++ b/app/logical/sources/strategies/hentai_foundry.rb @@ -23,11 +23,11 @@ module Sources module Strategies class HentaiFoundry < Base - BASE_URL = %r!\Ahttps?://(?:www\.)?hentai-foundry\.com!i - PAGE_URL = %r!#{BASE_URL}/pictures/user/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:/[\w.-]*)?(\?[\w=]*)?\z!i - OLD_PAGE = %r!#{BASE_URL}/pic-(?<illust_id>\d+)(?:\.html)?\z!i - PROFILE_URL = %r!#{BASE_URL}/(?:pictures/)?user/(?<artist_name>[\w-]+)(?:/[a-z]*)?\z!i - IMAGE_URL = %r!\Ahttps?://pictures\.hentai-foundry\.com/+\w/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:(?:/[\w.-]+)?\.\w+)?\z!i + BASE_URL = %r{\Ahttps?://(?:www\.)?hentai-foundry\.com}i + PAGE_URL = %r{#{BASE_URL}/pictures/user/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:/[\w.-]*)?(\?[\w=]*)?\z}i + OLD_PAGE = %r{#{BASE_URL}/pic-(?<illust_id>\d+)(?:\.html)?\z}i + PROFILE_URL = %r{#{BASE_URL}/(?:pictures/)?user/(?<artist_name>[\w-]+)(?:/[a-z]*)?\z}i + IMAGE_URL = %r{\Ahttps?://pictures\.hentai-foundry\.com/+\w/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:(?:/[\w.-]+)?\.\w+)?\z}i def domains ["hentai-foundry.com"] @@ -64,11 +64,10 @@ module Sources def page return nil if page_url.blank? - doc = Cache.get("hentai-foundry:#{page_url}", 1.minute) do - HTTParty.get("#{page_url}?enterAgree=1").body - end + response = Danbooru::Http.new.cache(1.minute).get("#{page_url}?enterAgree=1") + return nil unless response.status == 200 - Nokogiri::HTML(doc) + response.parse end def tags diff --git a/app/logical/sources/strategies/moebooru.rb b/app/logical/sources/strategies/moebooru.rb index af149682e..4a47b8d54 100644 --- a/app/logical/sources/strategies/moebooru.rb +++ b/app/logical/sources/strategies/moebooru.rb @@ -32,10 +32,10 @@ module Sources module Strategies class Moebooru < Base - BASE_URL = %r!\Ahttps?://(?:[^.]+\.)?(?<domain>yande\.re|konachan\.com)!i - POST_URL = %r!#{BASE_URL}/post/show/(?<id>\d+)!i - URL_SLUG = %r!/(?:yande\.re%20|Konachan\.com%20-%20)?(?<id>\d+)?.*!i - IMAGE_URL = %r!#{BASE_URL}/(?<type>image|jpeg|sample)/(?<md5>\h{32})#{URL_SLUG}?\.(?<ext>jpg|jpeg|png|gif)\z!i + BASE_URL = %r{\Ahttps?://(?:[^.]+\.)?(?<domain>yande\.re|konachan\.com)}i + POST_URL = %r{#{BASE_URL}/post/show/(?<id>\d+)}i + URL_SLUG = %r{/(?:yande\.re%20|Konachan\.com%20-%20)?(?<id>\d+)?.*}i + IMAGE_URL = %r{#{BASE_URL}/(?<type>image|jpeg|sample)/(?<md5>\h{32})#{URL_SLUG}?\.(?<ext>jpg|jpeg|png|gif)\z}i delegate :artist_name, :profile_url, :tag_name, :artist_commentary_title, :artist_commentary_desc, :dtext_artist_commentary_title, :dtext_artist_commentary_desc, to: :sub_strategy, allow_nil: true @@ -63,7 +63,7 @@ module Sources end def preview_urls - return image_urls unless post_md5.present? + return image_urls if post_md5.blank? ["https://#{file_host}/data/preview/#{post_md5[0..1]}/#{post_md5[2..3]}/#{post_md5}.jpg"] end @@ -155,7 +155,7 @@ module Sources # the api_response wasn't available because it's a deleted post. elsif post_md5.present? - %w[jpg png gif].find { |ext| http_exists?("https://#{site_name}/image/#{post_md5}.#{ext}", headers) } + %w[jpg png gif].find { |ext| http_exists?("https://#{site_name}/image/#{post_md5}.#{ext}") } else nil diff --git a/app/logical/sources/strategies/newgrounds.rb b/app/logical/sources/strategies/newgrounds.rb new file mode 100644 index 000000000..0fefba41d --- /dev/null +++ b/app/logical/sources/strategies/newgrounds.rb @@ -0,0 +1,111 @@ +# Image Urls +# * https://art.ngfiles.com/images/1254000/1254722_natthelich_pandora.jpg +# * https://art.ngfiles.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic.png?f1569487181 +# * https://art.ngfiles.com/comments/57000/iu_57615_7115981.jpg +# +# Page URLs +# * https://www.newgrounds.com/art/view/puddbytes/costanza-at-bat +# * https://www.newgrounds.com/art/view/natthelich/fire-emblem-marth-plus-progress-pic (multiple) +# +# Profile URLs +# * https://natthelich.newgrounds.com/ + +module Sources + module Strategies + class Newgrounds < Base + IMAGE_URL = %r{\Ahttps?://art\.ngfiles\.com/images/\d+/\d+_(?<user_name>[0-9a-z-]+)_(?<illust_title>[0-9a-z-]+)\.\w+}i + COMMENT_URL = %r{\Ahttps?://art\.ngfiles\.com/comments/\d+/\w+\.\w+}i + + PAGE_URL = %r{\Ahttps?://(?:www\.)?newgrounds\.com/art/view/(?<user_name>[0-9a-z-]+)/(?<illust_title>[0-9a-z-]+)(?:\?.*)?}i + + PROFILE_URL = %r{\Ahttps?://(?<artist_name>(?!www)[0-9a-z-]+)\.newgrounds\.com(?:/.*)?}i + + def domains + ["newgrounds.com", "ngfiles.com"] + end + + def site_name + "NewGrounds" + end + + def image_urls + if url =~ COMMENT_URL || url =~ IMAGE_URL + [url] + else + urls = [] + + urls += page&.css(".image img").to_a.map { |img| img["src"] } + urls += page&.css("#author_comments img[data-user-image='1']").to_a.map { |img| img["data-smartload-src"] || img["src"] } + + urls.compact + end + end + + def page_url + return nil if illust_title.blank? || user_name.blank? + + "https://www.newgrounds.com/art/view/#{user_name}/#{illust_title}" + end + + def page + return nil if page_url.blank? + + response = Danbooru::Http.cache(1.minute).get(page_url) + return nil if response.status == 404 + + response.parse + end + memoize :page + + def tags + page&.css("#sidestats .tags a").to_a.map do |tag| + [tag.text, "https://www.newgrounds.com/search/conduct/art?match=tags&tags=" + tag.text] + end + end + + def normalize_tag(tag) + tag = tag.tr("-", "_") + super(tag) + end + + def artist_name + name = page&.css(".item-user .item-details h4 a")&.text&.strip || user_name + name&.downcase + end + + def other_names + [artist_name, user_name].compact.uniq + end + + def profile_url + # user names are not mutable, artist names are. + # However we need the latest name for normalization + "https://#{artist_name}.newgrounds.com" + end + + def artist_commentary_title + page&.css(".pod-head > [itemprop='name']")&.text + end + + def artist_commentary_desc + page&.css("#author_comments")&.to_html + end + + def dtext_artist_commentary_desc + DText.from_html(artist_commentary_desc) + end + + def normalize_for_source + page_url + end + + def user_name + urls.map { |u| url[PROFILE_URL, :artist_name] || u[IMAGE_URL, :user_name] || u[PAGE_URL, :user_name] }.compact.first + end + + def illust_title + urls.map { |u| u[IMAGE_URL, :illust_title] || u[PAGE_URL, :illust_title] }.compact.first + end + end + end +end diff --git a/app/logical/sources/strategies/nico_seiga.rb b/app/logical/sources/strategies/nico_seiga.rb index 32a3fd65f..93a2d995c 100644 --- a/app/logical/sources/strategies/nico_seiga.rb +++ b/app/logical/sources/strategies/nico_seiga.rb @@ -1,25 +1,54 @@ -# Image Direct URL +# Direct URL # * https://lohas.nicoseiga.jp/o/971eb8af9bbcde5c2e51d5ef3a2f62d6d9ff5552/1589933964/3583893 # * http://lohas.nicoseiga.jp/priv/3521156?e=1382558156&h=f2e089256abd1d453a455ec8f317a6c703e2cedf # * http://lohas.nicoseiga.jp/priv/b80f86c0d8591b217e7513a9e175e94e00f3c7a1/1384936074/3583893 +# * https://dcdn.cdn.nimg.jp/priv/62a56a7f67d3d3746ae5712db9cac7d465f4a339/1592186183/10466669 +# * https://dcdn.cdn.nimg.jp/nicoseiga/lohas/o/8ba0a9b2ea34e1ef3b5cc50785bd10cd63ec7e4a/1592187477/10466669 +# +# * http://lohas.nicoseiga.jp/material/5746c5/4459092 +# +# (Manga direct url) +# * https://lohas.nicoseiga.jp/priv/f5b8966fd53bf7e06cccff9fbb2c4eef62877538/1590752727/8947170 +# +# Samples +# * http://lohas.nicoseiga.jp/thumb/2163478i? +# * https://lohas.nicoseiga.jp/thumb/8947170p +# +## The direct urls and samples above can belong to both illust and manga. +## There's two ways to tell them apart: +## * visit the /source/ equivalent: illusts redirect to the /o/ intermediary page, manga redirect to /priv/ directly +## * try an api call: illusts will succeed, manga will fail +# +# Source Link # * http://seiga.nicovideo.jp/image/source?id=3312222 # -# Image Page URL +# Illust Page URL # * https://seiga.nicovideo.jp/seiga/im3521156 +# * https://seiga.nicovideo.jp/seiga/im520647 (anonymous artist) # # Manga Page URL # * http://seiga.nicovideo.jp/watch/mg316708 +# +# Video Page URL (not supported) +# * https://www.nicovideo.jp/watch/sm36465441 +# +# Oekaki +# * https://dic.nicovideo.jp/oekaki/52833.png module Sources module Strategies class NicoSeiga < Base - URL = %r!\Ahttps?://(?:\w+\.)?nico(?:seiga|video)\.jp! - DIRECT1 = %r!\Ahttps?://lohas\.nicoseiga\.jp/priv/[0-9a-f]+! - DIRECT2 = %r!\Ahttps?://lohas\.nicoseiga\.jp/o/[0-9a-f]+/\d+/\d+! - DIRECT3 = %r!\Ahttps?://seiga\.nicovideo\.jp/images/source/\d+! - PAGE = %r!\Ahttps?://seiga\.nicovideo\.jp/seiga/im(\d+)!i - PROFILE = %r!\Ahttps?://seiga\.nicovideo\.jp/user/illust/(\d+)!i - MANGA_PAGE = %r!\Ahttps?://seiga\.nicovideo\.jp/watch/mg(\d+)!i + DIRECT = %r{\Ahttps?://lohas\.nicoseiga\.jp/(?:priv|o)/(?:\w+/\d+/)?(?<image_id>\d+)(?:\?.+)?}i + CDN_DIRECT = %r{\Ahttps?://dcdn\.cdn\.nimg\.jp/.+/\w+/\d+/(?<image_id>\d+)}i + SOURCE = %r{\Ahttps?://seiga\.nicovideo\.jp/image/source(?:/|\?id=)(?<image_id>\d+)}i + + ILLUST_THUMB = %r{\Ahttps?://lohas\.nicoseiga\.jp/thumb/(?<illust_id>\d+)i}i + MANGA_THUMB = %r{\Ahttps?://lohas\.nicoseiga\.jp/thumb/(?<image_id>\d+)p}i + + ILLUST_PAGE = %r{\Ahttps?://(?:sp\.)?seiga\.nicovideo\.jp/seiga/im(?<illust_id>\d+)}i + MANGA_PAGE = %r{\Ahttps?://(?:sp\.)?seiga\.nicovideo\.jp/watch/mg(?<manga_id>\d+)}i + + PROFILE_PAGE = %r{\Ahttps?://seiga\.nicovideo\.jp/user/illust/(?<artist_id>\d+)}i def domains ["nicoseiga.jp", "nicovideo.jp"] @@ -30,160 +59,136 @@ module Sources end def image_urls - if url =~ DIRECT1 - return [url] + urls = [] + return urls if api_client&.api_response.blank? + + if image_id.present? + urls << "https://seiga.nicovideo.jp/image/source/#{image_id}" + elsif illust_id.present? + urls << "https://seiga.nicovideo.jp/image/source/#{illust_id}" + elsif manga_id.present? && api_client.image_ids.present? + urls += api_client.image_ids.map { |id| "https://seiga.nicovideo.jp/image/source/#{id}" } + end + urls + end + + def image_url + return url if image_urls.blank? || api_client.blank? + + img = case url + when DIRECT || CDN_DIRECT then "https://seiga.nicovideo.jp/image/source/#{image_id_from_url(url)}" + when SOURCE then url + else image_urls.first end - if theme_id - return api_client.image_ids.map do |image_id| - "https://seiga.nicovideo.jp/image/source/#{image_id}" - end + resp = api_client.login.head(img) + if resp.uri.to_s =~ %r{https?://.+/(\w+/\d+/\d+)\z}i + "https://lohas.nicoseiga.jp/priv/#{$1}" + else + img end + end - link = page.search("a#illust_link") - - if link.any? - image_url = "http://seiga.nicovideo.jp" + link[0]["href"] - page = agent.get(image_url) # need to follow this redirect while logged in or it won't work - - if page.is_a?(Mechanize::Image) - return [page.uri.to_s] - end - - images = page.search("div.illust_view_big").select {|x| x["data-src"] =~ /\/priv\//} - - if images.any? - return ["http://lohas.nicoseiga.jp" + images[0]["data-src"]] - end + def preview_urls + if illust_id.present? + ["https://lohas.nicoseiga.jp/thumb/#{illust_id}i"] + else + image_urls end - - raise "image url not found for (#{url}, #{referer_url})" end def page_url - [url, referer_url].each do |x| - if x =~ %r!\Ahttps?://lohas\.nicoseiga\.jp/o/[a-f0-9]+/\d+/(\d+)! - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end - - if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/(\d+)\?e=\d+&h=[a-f0-9]+}i - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end - - if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/[a-f0-9]+/\d+/(\d+)}i - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end - - if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/(\d+)}i - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end - - if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp//?thumb/(\d+)i?}i - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end - - if x =~ %r{/seiga/im\d+} - return x - end - - if x =~ %r{/watch/mg\d+} - return x - end - - if x =~ %r{/image/source\?id=(\d+)} - return "http://seiga.nicovideo.jp/seiga/im#{$1}" - end + if illust_id.present? + "https://seiga.nicovideo.jp/seiga/im#{illust_id}" + elsif manga_id.present? + "https://seiga.nicovideo.jp/watch/mg#{manga_id}" + elsif image_id.present? + "https://seiga.nicovideo.jp/image/source/#{image_id}" end - - return super - end - - def canonical_url - image_url end def profile_url - if url =~ PROFILE - return url - end + user_id = api_client&.user_id + return if user_id.blank? # artists can be anonymous "http://seiga.nicovideo.jp/user/illust/#{api_client.user_id}" end def artist_name - api_client.moniker + return if api_client.blank? + api_client.user_name end def artist_commentary_title + return if api_client.blank? api_client.title end def artist_commentary_desc - api_client.desc + return if api_client.blank? + api_client.description + end + + def dtext_artist_commentary_desc + DText.from_html(artist_commentary_desc).gsub(/[^\w]im(\d+)/, ' seiga #\1 ') end def normalize_for_source - if illust_id.present? - "https://seiga.nicovideo.jp/seiga/im#{illust_id}" - elsif theme_id.present? - "http://seiga.nicovideo.jp/watch/mg#{theme_id}" + # There's no way to tell apart illust from manga from the direct image url alone. What's worse, + # nicoseiga itself doesn't know how to normalize back to manga, so if it's not an illust type then + # it's impossible to get the original manga page back from the image url alone. + # /source/ links on the other hand correctly redirect, hence we use them to normalize saved direct sources. + if url =~ DIRECT + "https://seiga.nicovideo.jp/image/source/#{image_id}" + else + page_url end end def tag_name + return if api_client&.user_id.blank? "nicoseiga#{api_client.user_id}" end def tags - string = page.at("meta[name=keywords]").try(:[], "content") || "" - string.split(/,/).map do |name| - [name, "https://seiga.nicovideo.jp/tag/#{CGI.escape(name)}"] + return [] if api_client.blank? + + base_url = "https://seiga.nicovideo.jp/" + base_url += "manga/" if manga_id.present? + base_url += "tag/" + + api_client.tags.map do |name| + [name, base_url + CGI.escape(name)] end end - memoize :tags + + def image_id + image_id_from_url(url) + end + + def image_id_from_url(url) + url[DIRECT, :image_id] || url[SOURCE, :image_id] || url[MANGA_THUMB, :image_id] || url[CDN_DIRECT, :image_id] + end + + def illust_id + urls.map { |u| u[ILLUST_PAGE, :illust_id] || u[ILLUST_THUMB, :illust_id] }.compact.first + end + + def manga_id + urls.compact.map { |u| u[MANGA_PAGE, :manga_id] }.compact.first + end def api_client - if illust_id - NicoSeigaApiClient.new(illust_id: illust_id) - elsif theme_id - NicoSeigaMangaApiClient.new(theme_id) + if illust_id.present? + NicoSeigaApiClient.new(work_id: illust_id, type: "illust", http: http) + elsif manga_id.present? + NicoSeigaApiClient.new(work_id: manga_id, type: "manga", http: http) + elsif image_id.present? + # We default to illust to attempt getting the api anyway + NicoSeigaApiClient.new(work_id: image_id, type: "illust", http: http) end end memoize :api_client - - def illust_id - if page_url =~ PAGE - return $1.to_i - end - - return nil - end - - def theme_id - if page_url =~ MANGA_PAGE - return $1.to_i - end - - return nil - end - - def page - doc = agent.get(page_url) - - if doc.search("a#link_btn_login").any? - # Session cache is invalid, clear it and log in normally. - Cache.delete("nico-seiga-session") - doc = agent.get(page_url) - end - - doc - end - memoize :page - - def agent - NicoSeigaApiClient.agent - end - memoize :agent end end end diff --git a/app/logical/sources/strategies/nijie.rb b/app/logical/sources/strategies/nijie.rb index e41d47f3d..4556f274a 100644 --- a/app/logical/sources/strategies/nijie.rb +++ b/app/logical/sources/strategies/nijie.rb @@ -44,25 +44,25 @@ module Sources module Strategies class Nijie < Base - BASE_URL = %r!\Ahttps?://(?:[^.]+\.)?nijie\.info!i - PAGE_URL = %r!#{BASE_URL}/view(?:_popup)?\.php\?id=(?<illust_id>\d+)!i - PROFILE_URL = %r!#{BASE_URL}/members(?:_illust)?\.php\?id=(?<artist_id>\d+)\z!i + BASE_URL = %r{\Ahttps?://(?:[^.]+\.)?nijie\.info}i + PAGE_URL = %r{#{BASE_URL}/view(?:_popup)?\.php\?id=(?<illust_id>\d+)}i + PROFILE_URL = %r{#{BASE_URL}/members(?:_illust)?\.php\?id=(?<artist_id>\d+)\z}i # https://pic03.nijie.info/nijie_picture/28310_20131101215959.jpg # https://pic03.nijie.info/nijie_picture/236014_20170620101426_0.png # http://pic.nijie.net/03/nijie_picture/829001_20190620004513_0.mp4 # https://pic05.nijie.info/nijie_picture/diff/main/559053_20180604023346_1.png - FILENAME1 = %r!(?<artist_id>\d+)_(?<timestamp>\d{14})(?:_\d+)?!i + FILENAME1 = /(?<artist_id>\d+)_(?<timestamp>\d{14})(?:_\d+)?/i # https://pic01.nijie.info/nijie_picture/diff/main/218856_0_236014_20170620101329.png - FILENAME2 = %r!(?<illust_id>\d+)_\d+_(?<artist_id>\d+)_(?<timestamp>\d{14})!i + FILENAME2 = /(?<illust_id>\d+)_\d+_(?<artist_id>\d+)_(?<timestamp>\d{14})/i # https://pic04.nijie.info/nijie_picture/diff/main/287736_161475_20181112032855_1.png - FILENAME3 = %r!(?<illust_id>\d+)_(?<artist_id>\d+)_(?<timestamp>\d{14})_\d+!i + FILENAME3 = /(?<illust_id>\d+)_(?<artist_id>\d+)_(?<timestamp>\d{14})_\d+/i - IMAGE_BASE_URL = %r!\Ahttps?://(?:pic\d+\.nijie\.info|pic\.nijie\.net)!i - DIR = %r!(?:\d+/)?(?:__rs_\w+/)?nijie_picture(?:/diff/main)?! - IMAGE_URL = %r!#{IMAGE_BASE_URL}/#{DIR}/#{Regexp.union(FILENAME1, FILENAME2, FILENAME3)}\.\w+\z!i + IMAGE_BASE_URL = %r{\Ahttps?://(?:pic\d+\.nijie\.info|pic\.nijie\.net)}i + DIR = %r{(?:\d+/)?(?:__rs_\w+/)?nijie_picture(?:/diff/main)?} + IMAGE_URL = %r{#{IMAGE_BASE_URL}/#{DIR}/#{Regexp.union(FILENAME1, FILENAME2, FILENAME3)}\.\w+\z}i def domains ["nijie.info", "nijie.net"] @@ -146,7 +146,7 @@ module Sources end def to_full_image_url(x) - x.gsub(%r!__rs_\w+/!i, "").gsub(/\Ahttp:/, "https:") + x.gsub(%r{__rs_\w+/}i, "").gsub(/\Ahttp:/, "https:") end def to_preview_url(url) @@ -178,57 +178,21 @@ module Sources def page return nil if page_url.blank? - doc = agent.get(page_url) + http = Danbooru::Http.new + form = { email: Danbooru.config.nijie_login, password: Danbooru.config.nijie_password } - if doc.search("div#header-login-container").any? - # Session cache is invalid, clear it and log in normally. - Cache.delete("nijie-session") - doc = agent.get(page_url) - end + # XXX `retriable` must come after `cache` so that retries don't return cached error responses. + response = http.cache(1.hour).use(retriable: { max_retries: 20 }).post("https://nijie.info/login_int.php", form: form) + DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200 + return nil unless response.status == 200 - return doc - rescue Mechanize::ResponseCodeError => e - return nil if e.response_code.to_i == 404 - raise + response = http.cookies(R18: 1).cache(1.minute).get(page_url) + return nil unless response.status == 200 + + response&.parse end + memoize :page - - def agent - mech = Mechanize.new - - session = Cache.get("nijie-session") - if session - cookie = Mechanize::Cookie.new("NIJIEIJIEID", session) - cookie.domain = ".nijie.info" - cookie.path = "/" - mech.cookie_jar.add(cookie) - else - mech.get("https://nijie.info/login.php") do |page| - page.form_with(:action => "/login_int.php") do |form| - form['email'] = Danbooru.config.nijie_login - form['password'] = Danbooru.config.nijie_password - end.click_button - end - session = mech.cookie_jar.cookies.select {|c| c.name == "NIJIEIJIEID"}.first - Cache.put("nijie-session", session.value, 1.day) if session - end - - # This cookie needs to be set to allow viewing of adult works while anonymous - cookie = Mechanize::Cookie.new("R18", "1") - cookie.domain = ".nijie.info" - cookie.path = "/" - mech.cookie_jar.add(cookie) - - mech - rescue Mechanize::ResponseCodeError => x - if x.response_code.to_i == 429 - sleep(5) - retry - else - raise - end - end - memoize :agent end end end diff --git a/app/logical/sources/strategies/null.rb b/app/logical/sources/strategies/null.rb index e17c5e5c4..4dd02ebe6 100644 --- a/app/logical/sources/strategies/null.rb +++ b/app/logical/sources/strategies/null.rb @@ -28,7 +28,7 @@ module Sources when %r{\Ahttp://p\.twpl\.jp/show/(?:large|orig)/([a-z0-9]+)}i "http://p.twipple.jp/#{$1}" - when %r{\Ahttps?://blog(?:(?:-imgs-)?\d*(?:-origin)?)?\.fc2\.com/(?:(?:[^/]/){3}|(?:[^/]/))([^/]+)/(?:file/)?([^\.]+\.[^\?]+)}i + when %r{\Ahttps?://blog(?:(?:-imgs-)?\d*(?:-origin)?)?\.fc2\.com/(?:(?:[^/]/){3}|(?:[^/]/))([^/]+)/(?:file/)?([^.]+\.[^?]+)}i username = $1 filename = $2 "http://#{username}.blog.fc2.com/img/#{filename}/" @@ -47,7 +47,7 @@ module Sources when %r{\Ahttps?://c(?:s|han|[1-4])\.sankakucomplex\.com/data(?:/sample)?/(?:[a-f0-9]{2}/){2}(?:sample-|preview)?([a-f0-9]{32})}i "https://chan.sankakucomplex.com/en/post/show?md5=#{$1}" - when %r{\Ahttps?://(?:www|s(?:tatic|[1-4]))\.zerochan\.net/.+(?:\.|\/)(\d+)(?:\.(?:jpe?g?))?\z}i + when %r{\Ahttps?://(?:www|s(?:tatic|[1-4]))\.zerochan\.net/.+(?:\.|\/)(\d+)(?:\.(?:jpe?g?|png))?\z}i "https://www.zerochan.net/#{$1}#full" when %r{\Ahttps?://static[1-6]?\.minitokyo\.net/(?:downloads|view)/(?:\d{2}/){2}(\d+)}i @@ -105,7 +105,7 @@ module Sources # http://img.toranoana.jp/popup_img18/04/0010/22/87/040010228714-1p.jpg # http://img.toranoana.jp/popup_blimg/04/0030/08/30/040030083068-1p.jpg # https://ecdnimg.toranoana.jp/ec/img/04/0030/65/34/040030653417-6p.jpg - when %r{\Ahttps?://(\w+\.)?toranoana\.jp/(?:popup_(?:bl)?img\d*|ec/img)/\d{2}/\d{4}/\d{2}/\d{2}/(?<work_id>\d+)}i + when %r{\Ahttps?://(?:\w+\.)?toranoana\.jp/(?:popup_(?:bl)?img\d*|ec/img)/\d{2}/\d{4}/\d{2}/\d{2}/(?<work_id>\d+)}i "https://ec.toranoana.jp/tora_r/ec/item/#{$~[:work_id]}/" # https://a.hitomi.la/galleries/907838/1.png diff --git a/app/logical/sources/strategies/pawoo.rb b/app/logical/sources/strategies/pawoo.rb index 49a04b5ae..3af7ef8a7 100644 --- a/app/logical/sources/strategies/pawoo.rb +++ b/app/logical/sources/strategies/pawoo.rb @@ -16,13 +16,13 @@ module Sources::Strategies class Pawoo < Base - HOST = %r!\Ahttps?://(www\.)?pawoo\.net!i - IMAGE = %r!\Ahttps?://img\.pawoo\.net/media_attachments/files/(\d+/\d+/\d+)! - NAMED_PROFILE = %r!#{HOST}/@(?<artist_name>\w+)!i - ID_PROFILE = %r!#{HOST}/web/accounts/(?<artist_id>\d+)! + HOST = %r{\Ahttps?://(www\.)?pawoo\.net}i + IMAGE = %r{\Ahttps?://img\.pawoo\.net/media_attachments/files/(\d+/\d+/\d+)} + NAMED_PROFILE = %r{#{HOST}/@(?<artist_name>\w+)}i + ID_PROFILE = %r{#{HOST}/web/accounts/(?<artist_id>\d+)} - STATUS1 = %r!\A#{HOST}/web/statuses/(?<status_id>\d+)! - STATUS2 = %r!\A#{NAMED_PROFILE}/(?<status_id>\d+)! + STATUS1 = %r{\A#{HOST}/web/statuses/(?<status_id>\d+)} + STATUS2 = %r{\A#{NAMED_PROFILE}/(?<status_id>\d+)} def domains ["pawoo.net"] @@ -37,15 +37,13 @@ module Sources::Strategies end def image_urls - if url =~ %r!#{IMAGE}/small/([a-z0-9]+\.\w+)\z!i - return ["https://img.pawoo.net/media_attachments/files/#{$1}/original/#{$2}"] + if url =~ %r{#{IMAGE}/small/([a-z0-9]+\.\w+)\z}i + ["https://img.pawoo.net/media_attachments/files/#{$1}/original/#{$2}"] + elsif url =~ %r{#{IMAGE}/original/([a-z0-9]+\.\w+)\z}i + [url] + else + api_response.image_urls end - - if url =~ %r!#{IMAGE}/original/([a-z0-9]+\.\w+)\z!i - return [url] - end - - return api_response.image_urls end def page_url @@ -55,16 +53,17 @@ module Sources::Strategies end end - return super + super end def profile_url if url =~ PawooApiClient::PROFILE2 - return "https://pawoo.net/@#{$1}" + "https://pawoo.net/@#{$1}" + elsif api_response.profile_url.blank? + url + else + api_response.profile_url end - - return url if api_response.profile_url.blank? - api_response.profile_url end def artist_name @@ -87,10 +86,6 @@ module Sources::Strategies urls.map { |url| url[STATUS1, :status_id] || url[STATUS2, :status_id] }.compact.first end - def artist_commentary_title - nil - end - def artist_commentary_desc api_response.commentary end @@ -99,18 +94,10 @@ module Sources::Strategies api_response.tags end - def normalizable_for_artist_finder? - true - end - - def normalize_for_artist_finder - profile_url - end - def normalize_for_source artist_name = artist_name_from_url status_id = status_id_from_url - return unless status_id.present? + return if status_id.blank? if artist_name.present? "https://pawoo.net/@#{artist_name}/#{status_id}" @@ -131,7 +118,7 @@ module Sources::Strategies def api_response [url, referer_url].each do |x| - if client = PawooApiClient.new.get(x) + if (client = PawooApiClient.new.get(x)) return client end end diff --git a/app/logical/sources/strategies/pixiv.rb b/app/logical/sources/strategies/pixiv.rb index 5f2bbf669..710a9c64f 100644 --- a/app/logical/sources/strategies/pixiv.rb +++ b/app/logical/sources/strategies/pixiv.rb @@ -50,37 +50,34 @@ module Sources module Strategies class Pixiv < Base - MONIKER = %r!(?:[a-zA-Z0-9_-]+)! - PROFILE = %r!\Ahttps?://www\.pixiv\.net/member\.php\?id=[0-9]+\z! - DATE = %r!(?<date>\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2})!i - EXT = %r!(?:jpg|jpeg|png|gif)!i + MONIKER = /(?:[a-zA-Z0-9_-]+)/ + PROFILE = %r{\Ahttps?://www\.pixiv\.net/member\.php\?id=[0-9]+\z} + DATE = %r{(?<date>\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2})}i + EXT = /(?:jpg|jpeg|png|gif)/i - WEB = %r!(?:\A(?:https?://)?www\.pixiv\.net)! - I12 = %r!(?:\A(?:https?://)?i[0-9]+\.pixiv\.net)! - IMG = %r!(?:\A(?:https?://)?img[0-9]*\.pixiv\.net)! - PXIMG = %r!(?:\A(?:https?://)?[^.]+\.pximg\.net)! - TOUCH = %r!(?:\A(?:https?://)?touch\.pixiv\.net)! - UGOIRA = %r!#{PXIMG}/img-zip-ugoira/img/#{DATE}/(?<illust_id>\d+)_ugoira1920x1080\.zip\z!i - ORIG_IMAGE = %r!#{PXIMG}/img-original/img/#{DATE}/(?<illust_id>\d+)_p(?<page>\d+)\.#{EXT}\z!i - STACC_PAGE = %r!\A#{WEB}/stacc/#{MONIKER}/?\z!i - NOVEL_PAGE = %r!(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))! - FANBOX_ACCOUNT = %r!(?:\Ahttps?://www\.pixiv\.net/fanbox/creator/\d+\z)! - FANBOX_IMAGE = %r!(?:\Ahttps?://fanbox\.pixiv\.net/images/post/(\d+))! - FANBOX_PAGE = %r!(?:\Ahttps?://www\.pixiv\.net/fanbox/creator/\d+/post/(\d+))! + WEB = %r{(?:\A(?:https?://)?www\.pixiv\.net)} + I12 = %r{(?:\A(?:https?://)?i[0-9]+\.pixiv\.net)} + IMG = %r{(?:\A(?:https?://)?img[0-9]*\.pixiv\.net)} + PXIMG = %r{(?:\A(?:https?://)?[^.]+\.pximg\.net)} + TOUCH = %r{(?:\A(?:https?://)?touch\.pixiv\.net)} + UGOIRA = %r{#{PXIMG}/img-zip-ugoira/img/#{DATE}/(?<illust_id>\d+)_ugoira1920x1080\.zip\z}i + ORIG_IMAGE = %r{#{PXIMG}/img-original/img/#{DATE}/(?<illust_id>\d+)_p(?<page>\d+)\.#{EXT}\z}i + STACC_PAGE = %r{\A#{WEB}/stacc/#{MONIKER}/?\z}i + NOVEL_PAGE = %r{(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))} def self.to_dtext(text) if text.nil? return nil end - 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 ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}]) 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 - profile_url = "https://www.pixiv.net/member.php?id=#{member_id}" + profile_url = "https://www.pixiv.net/users/#{member_id}" search_params = {"search[url_matches]" => profile_url}.to_param %("user/#{member_id}":[#{profile_url}] "»":[/artists?#{search_params}]) @@ -127,25 +124,17 @@ module Sources return "https://www.pixiv.net/novel/show.php?id=#{novel_id}&mode=cover" end - if fanbox_id.present? - return "https://www.pixiv.net/fanbox/creator/#{metadata.user_id}/post/#{fanbox_id}" - end - - if fanbox_account_id.present? - return "https://www.pixiv.net/fanbox/creator/#{fanbox_account_id}" - end - if illust_id.present? return "https://www.pixiv.net/artworks/#{illust_id}" end - return url + url rescue PixivApiClient::BadIDError nil end def canonical_url - return image_url + image_url end def profile_url @@ -155,7 +144,7 @@ module Sources end end - "https://www.pixiv.net/member.php?id=#{metadata.user_id}" + "https://www.pixiv.net/users/#{metadata.user_id}" rescue PixivApiClient::BadIDError nil end @@ -192,17 +181,7 @@ module Sources end def headers - if fanbox_id.present? - # need the session to download fanbox images - return { - "Referer" => "https://www.pixiv.net/fanbox", - "Cookie" => HTTP::Cookie.cookie_value(agent.cookies) - } - end - - return { - "Referer" => "https://www.pixiv.net" - } + { "Referer" => "https://www.pixiv.net" } end def normalize_for_source @@ -231,7 +210,7 @@ module Sources translated_tags = super(tag) if translated_tags.empty? && tag.include?("/") - translated_tags = tag.split("/").flat_map { |tag| super(tag) } + translated_tags = tag.split("/").flat_map { |translated_tag| super(translated_tag) } end translated_tags @@ -242,10 +221,6 @@ module Sources end def image_urls_sub - if url =~ FANBOX_IMAGE - return [url] - end - # there's too much normalization bullshit we have to deal with # raw urls, so just fetch the canonical url from the api every # time. @@ -257,7 +232,7 @@ module Sources return [ugoira_zip_url] end - return metadata.pages + metadata.pages end # in order to prevent recursive loops, this method should not make any @@ -265,7 +240,7 @@ module Sources # even though it makes sense to reference page_url here, it will only look # at (url, referer_url). def illust_id - return nil if novel_id.present? || fanbox_id.present? + return nil if novel_id.present? parsed_urls.each do |url| # http://www.pixiv.net/member_illust.php?mode=medium&illust_id=18557054 @@ -276,11 +251,11 @@ module Sources return url.query_values["illust_id"].to_i # http://www.pixiv.net/en/artworks/46324488 - elsif url.host == "www.pixiv.net" && url.path =~ %r!\A/(?:en/)?artworks/(?<illust_id>\d+)!i + elsif url.host == "www.pixiv.net" && url.path =~ %r{\A/(?:en/)?artworks/(?<illust_id>\d+)}i return $~[:illust_id].to_i # http://www.pixiv.net/i/18557054 - elsif url.host == "www.pixiv.net" && url.path =~ %r!\A/i/(?<illust_id>\d+)\z!i + elsif url.host == "www.pixiv.net" && url.path =~ %r{\A/i/(?<illust_id>\d+)\z}i return $~[:illust_id].to_i # http://img18.pixiv.net/img/evazion/14901720.png @@ -289,8 +264,8 @@ module Sources # http://i2.pixiv.net/img18/img/evazion/14901720_s.png # http://i1.pixiv.net/img07/img/pasirism/18557054_p1.png # http://i1.pixiv.net/img07/img/pasirism/18557054_big_p1.png - elsif url.host =~ %r!\A(?:i\d+|img\d+)\.pixiv\.net\z!i && - url.path =~ %r!\A(?:/img\d+)?/img/#{MONIKER}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)!i + elsif url.host =~ /\A(?:i\d+|img\d+)\.pixiv\.net\z/i && + url.path =~ %r{\A(?:/img\d+)?/img/#{MONIKER}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)}i return $~[:illust_id].to_i # http://i1.pixiv.net/img-inf/img/2011/05/01/23/28/04/18557054_64x64.jpg @@ -307,13 +282,13 @@ module Sources # # https://i.pximg.net/novel-cover-original/img/2019/01/14/01/15/05/10617324_d84daae89092d96bbe66efafec136e42.jpg # https://img-sketch.pixiv.net/uploads/medium/file/4463372/8906921629213362989.jpg - elsif url.host =~ %r!\A(?:[^.]+\.pximg\.net|i\d+\.pixiv\.net|tc-pximg01\.techorus-cdn\.com)\z!i && - url.path =~ %r!\A(/c/\w+)?/img-[a-z-]+/img/#{DATE}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)!i + elsif url.host =~ /\A(?:[^.]+\.pximg\.net|i\d+\.pixiv\.net|tc-pximg01\.techorus-cdn\.com)\z/i && + url.path =~ %r{\A(/c/\w+)?/img-[a-z-]+/img/#{DATE}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)}i return $~[:illust_id].to_i end end - return nil + nil end memoize :illust_id @@ -324,89 +299,48 @@ module Sources end end - return nil + nil end memoize :novel_id - def fanbox_id - [url, referer_url].each do |x| - if x =~ FANBOX_PAGE - return $1 - end - - if x =~ FANBOX_IMAGE - return $1 - end - end - - return nil - end - memoize :fanbox_id - - def fanbox_account_id - [url, referer_url].each do |x| - if x =~ FANBOX_ACCOUNT - return x - end - end - - return nil - end - memoize :fanbox_account_id - - def agent - PixivWebAgent.build - end - memoize :agent - def metadata if novel_id.present? return PixivApiClient.new.novel(novel_id) end - if fanbox_id.present? - return PixivApiClient.new.fanbox(fanbox_id) - end - - return PixivApiClient.new.work(illust_id) + PixivApiClient.new.work(illust_id) end memoize :metadata def moniker # we can sometimes get the moniker from the url - if url =~ %r!#{IMG}/img/(#{MONIKER})!i - return $1 + if url =~ %r{#{IMG}/img/(#{MONIKER})}i + $1 + elsif url =~ %r{#{I12}/img[0-9]+/img/(#{MONIKER})}i + $1 + elsif url =~ %r{#{WEB}/stacc/(#{MONIKER})/?$}i + $1 + else + metadata.moniker end - - if url =~ %r!#{I12}/img[0-9]+/img/(#{MONIKER})!i - return $1 - end - - if url =~ %r!#{WEB}/stacc/(#{MONIKER})/?$!i - return $1 - end - - return metadata.moniker rescue PixivApiClient::BadIDError nil end memoize :moniker def data - return { - ugoira_frame_data: ugoira_frame_data - } + { ugoira_frame_data: ugoira_frame_data } end def ugoira_zip_url if metadata.pages.is_a?(Hash) && metadata.pages["ugoira600x600"] - return metadata.pages["ugoira600x600"].sub("_ugoira600x600.zip", "_ugoira1920x1080.zip") + metadata.pages["ugoira600x600"].sub("_ugoira600x600.zip", "_ugoira1920x1080.zip") end end memoize :ugoira_zip_url def ugoira_frame_data - return metadata.json.dig("metadata", "frames") + metadata.json.dig("metadata", "frames") rescue PixivApiClient::BadIDError nil end @@ -415,16 +349,14 @@ module Sources def ugoira_content_type case metadata.json["image_urls"].to_s when /\.jpg/ - return "image/jpeg" - + "image/jpeg" when /\.png/ - return "image/png" - + "image/png" when /\.gif/ - return "image/gif" + "image/gif" + else + raise Sources::Error, "content type not found for (#{url}, #{referer_url})" end - - raise Sources::Error.new("content type not found for (#{url}, #{referer_url})") end memoize :ugoira_content_type @@ -434,7 +366,7 @@ module Sources # http://i2.pixiv.net/img04/img/syounen_no_uta/46170939_p0.jpg # http://i1.pixiv.net/c/600x600/img-master/img/2014/09/24/23/25/08/46168376_p0_master1200.jpg # http://i1.pixiv.net/img-original/img/2014/09/25/23/09/29/46183440_p0.jpg - if url =~ %r!/\d+_p(\d+)(?:_\w+)?\.#{EXT}!i + if url =~ %r{/\d+_p(\d+)(?:_\w+)?\.#{EXT}}i return $1.to_i end @@ -445,7 +377,7 @@ module Sources end end - return nil + nil end memoize :manga_page end diff --git a/app/logical/sources/strategies/tumblr.rb b/app/logical/sources/strategies/tumblr.rb index c6d2fc5bc..e3440c910 100644 --- a/app/logical/sources/strategies/tumblr.rb +++ b/app/logical/sources/strategies/tumblr.rb @@ -12,19 +12,19 @@ module Sources::Strategies class Tumblr < Base SIZES = %w[1280 640 540 500h 500 400 250 100] - BASE_URL = %r!\Ahttps?://(?:[^/]+\.)*tumblr\.com!i - DOMAIN = %r{(data|(\d+\.)?media)\.tumblr\.com} - MD5 = %r{(?<md5>[0-9a-f]{32})}i - FILENAME = %r{(?<filename>(tumblr_(inline_)?)?[a-z0-9]+(_r[0-9]+)?)}i - EXT = %r{(?<ext>\w+)} + BASE_URL = %r{\Ahttps?://(?:[^/]+\.)*tumblr\.com}i + DOMAIN = /(data|(?:\d+\.)?media)\.tumblr\.com/i + MD5 = /(?<md5>[0-9a-f]{32})/i + FILENAME = /(?<filename>(?:tumblr_(?:inline_)?)?[a-z0-9]+(?:_r[0-9]+)?)/i + EXT = /(?<ext>\w+)/ # old: https://66.media.tumblr.com/2c6f55531618b4335c67e29157f5c1fc/tumblr_pz4a44xdVj1ssucdno1_1280.png # new: https://66.media.tumblr.com/168dabd09d5ad69eb5fedcf94c45c31a/3dbfaec9b9e0c2e3-72/s640x960/bf33a1324f3f36d2dc64f011bfeab4867da62bc8.png - OLD_IMAGE = %r!\Ahttps?://#{DOMAIN}/(?<dir>#{MD5}/)?#{FILENAME}_(?<size>\w+)\.#{EXT}\z!i + OLD_IMAGE = %r{\Ahttps?://#{DOMAIN}/(?<dir>#{MD5}/)?#{FILENAME}_(?<size>\w+)\.#{EXT}\z}i - IMAGE = %r!\Ahttps?://#{DOMAIN}/!i - VIDEO = %r!\Ahttps?://(?:vtt|ve\.media)\.tumblr\.com/!i - POST = %r!\Ahttps?://(?<blog_name>[^.]+)\.tumblr\.com/(?:post|image)/(?<post_id>\d+)!i + IMAGE = %r{\Ahttps?://#{DOMAIN}/}i + VIDEO = %r{\Ahttps?://(?:vtt|ve|va\.media)\.tumblr\.com/}i + POST = %r{\Ahttps?://(?<blog_name>[^.]+)\.tumblr\.com/(?:post|image)/(?<post_id>\d+)}i def self.enabled? Danbooru.config.tumblr_consumer_key.present? @@ -68,7 +68,7 @@ module Sources::Strategies def preview_urls image_urls.map do |x| - x.sub(%r!_1280\.(jpg|png|gif|jpeg)\z!, '_250.\1') + x.sub(/_1280\.(jpg|png|gif|jpeg)\z/, '_250.\1') end end @@ -168,7 +168,7 @@ module Sources::Strategies end candidates.find do |candidate| - http_exists?(candidate, headers) + http_exists?(candidate) end end diff --git a/app/logical/sources/strategies/twitter.rb b/app/logical/sources/strategies/twitter.rb index 69f363363..503a96df0 100644 --- a/app/logical/sources/strategies/twitter.rb +++ b/app/logical/sources/strategies/twitter.rb @@ -1,20 +1,20 @@ module Sources::Strategies class Twitter < Base - PAGE = %r!\Ahttps?://(?:mobile\.)?twitter\.com!i - PROFILE = %r!\Ahttps?://(?:mobile\.)?twitter.com/(?<username>[a-z0-9_]+)!i + PAGE = %r{\Ahttps?://(?:mobile\.)?twitter\.com}i + PROFILE = %r{\Ahttps?://(?:mobile\.)?twitter.com/(?<username>[a-z0-9_]+)}i # https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb.jpg # https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb?format=jpg&name=900x900 # https://pbs.twimg.com/tweet_video_thumb/ETkN_L3X0AMy1aT.jpg # https://pbs.twimg.com/ext_tw_video_thumb/1243725361986375680/pu/img/JDA7g7lcw7wK-PIv.jpg # https://pbs.twimg.com/amplify_video_thumb/1215590775364259840/img/lolCkEEioFZTb5dl.jpg - BASE_IMAGE_URL = %r!\Ahttps?://pbs\.twimg\.com/(?<media_type>media|tweet_video_thumb|ext_tw_video_thumb|amplify_video_thumb)!i - FILENAME1 = %r!(?<file_name>[a-zA-Z0-9_-]+)\.(?<file_ext>\w+)!i - FILENAME2 = %r!(?<file_name>[a-zA-Z0-9_-]+)\?.*format=(?<file_ext>\w+)!i - FILEPATH1 = %r!(?<file_path>\d+/[\w_-]+/img)!i - FILEPATH2 = %r!(?<file_path>\d+/img)!i - IMAGE_URL1 = %r!#{BASE_IMAGE_URL}/#{Regexp.union(FILENAME1, FILENAME2)}!i - IMAGE_URL2 = %r!#{BASE_IMAGE_URL}/#{Regexp.union(FILEPATH1, FILEPATH2)}/#{FILENAME1}!i + BASE_IMAGE_URL = %r{\Ahttps?://pbs\.twimg\.com/(?<media_type>media|tweet_video_thumb|ext_tw_video_thumb|amplify_video_thumb)}i + FILENAME1 = /(?<file_name>[a-zA-Z0-9_-]+)\.(?<file_ext>\w+)/i + FILENAME2 = /(?<file_name>[a-zA-Z0-9_-]+)\?.*format=(?<file_ext>\w+)/i + FILEPATH1 = %r{(?<file_path>\d+/[\w_-]+/img)}i + FILEPATH2 = %r{(?<file_path>\d+/img)}i + IMAGE_URL1 = %r{#{BASE_IMAGE_URL}/#{Regexp.union(FILENAME1, FILENAME2)}}i + IMAGE_URL2 = %r{#{BASE_IMAGE_URL}/#{Regexp.union(FILEPATH1, FILEPATH2)}/#{FILENAME1}}i # Twitter provides a list but it's inaccurate; some names ('intent') aren't # included and other names in the list aren't actually reserved. @@ -47,7 +47,7 @@ module Sources::Strategies return $1 end - return nil + nil end def self.artist_name_from_url(url) @@ -78,7 +78,7 @@ module Sources::Strategies elsif media[:type].in?(["video", "animated_gif"]) variants = media.dig(:video_info, :variants) videos = variants.select { |variant| variant[:content_type] == "video/mp4" } - video = videos.max_by { |video| video[:bitrate].to_i } + video = videos.max_by { |v| v[:bitrate].to_i } video[:url] end end @@ -137,10 +137,6 @@ module Sources::Strategies api_response[:full_text].to_s end - def normalizable_for_artist_finder? - url =~ PAGE - end - def normalize_for_artist_finder profile_url.try(:downcase).presence || url end @@ -193,9 +189,9 @@ module Sources::Strategies desc = artist_commentary_desc.unicode_normalize(:nfkc) desc = CGI.unescapeHTML(desc) - desc = desc.gsub(%r!https?://t\.co/[a-zA-Z0-9]+!i, url_replacements) - desc = desc.gsub(%r!#([^[:space:]]+)!, '"#\\1":[https://twitter.com/hashtag/\\1]') - desc = desc.gsub(%r!@([a-zA-Z0-9_]+)!, '"@\\1":[https://twitter.com/\\1]') + desc = desc.gsub(%r{https?://t\.co/[a-zA-Z0-9]+}i, url_replacements) + desc = desc.gsub(/#([^[:space:]]+)/, '"#\\1":[https://twitter.com/hashtag/\\1]') + desc = desc.gsub(/@([a-zA-Z0-9_]+)/, '"@\\1":[https://twitter.com/\\1]') desc.strip end @@ -204,7 +200,7 @@ module Sources::Strategies end def api_response - return {} if !self.class.enabled? + return {} unless self.class.enabled? && status_id.present? api_client.status(status_id) end diff --git a/app/logical/sources/strategies/weibo.rb b/app/logical/sources/strategies/weibo.rb index 8cad13cd1..eaac6e40f 100644 --- a/app/logical/sources/strategies/weibo.rb +++ b/app/logical/sources/strategies/weibo.rb @@ -38,7 +38,7 @@ module Sources PAGE_URL_1 = %r{\Ahttps?://(?:www\.)?weibo\.com/(?<artist_short_id>\d+)/(?<illust_base62_id>\w+)(?:\?.*)?\z}i PAGE_URL_2 = %r{#{PROFILE_URL_2}/(?:wbphotos/large/mid|talbum/detail/photo_id)/(?<illust_long_id>\d+)(?:/pid/(?<image_id>\w{32}))?}i - PAGE_URL_3 = %r{\Ahttps?://m\.weibo\.cn/(detail/(?<illust_long_id>\d+)|status/(?<illust_base62_id>\w+))}i + PAGE_URL_3 = %r{\Ahttps?://m\.weibo\.cn/(?:detail/(?<illust_long_id>\d+)|status/(?<illust_base62_id>\w+))}i PAGE_URL_4 = %r{\Ahttps?://tw\.weibo\.com/(?:(?<artist_short_id>\d+)|\w+)/(?<illust_long_id>\d+)}i IMAGE_URL = %r{\Ahttps?://\w{3}\.sinaimg\.cn/\w+/(?<image_id>\w{32})\.}i @@ -203,12 +203,12 @@ module Sources end def api_response - return nil if mobile_url.blank? + return {} if mobile_url.blank? resp = Danbooru::Http.cache(1.minute).get(mobile_url) json_string = resp.to_s[/var \$render_data = \[(.*)\]\[0\]/m, 1] - return nil if json_string.blank? + return {} if json_string.blank? JSON.parse(json_string)["status"] end diff --git a/app/logical/spam_detector.rb b/app/logical/spam_detector.rb index a6ca4c2b0..176cc40d6 100644 --- a/app/logical/spam_detector.rb +++ b/app/logical/spam_detector.rb @@ -7,10 +7,11 @@ class SpamDetector # if a person receives more than 10 automatic spam reports within a 1 hour # window, automatically ban them forever. AUTOBAN_THRESHOLD = 10 - AUTOBAN_WINDOW = 1.hours - AUTOBAN_DURATION = 999999 + AUTOBAN_WINDOW = 1.hour + AUTOBAN_DURATION = 999_999 attr_accessor :record, :user, :user_ip, :content, :comment_type + rakismet_attrs author: proc { user.name }, author_email: proc { user.email_address&.address }, blog_lang: "en", @@ -84,8 +85,8 @@ class SpamDetector end is_spam - rescue StandardError => exception - DanbooruLogger.log(exception) + rescue StandardError => e + DanbooruLogger.log(e) false end end diff --git a/app/logical/storage_manager/sftp.rb b/app/logical/storage_manager/sftp.rb index 1eeb1fdce..ef2a2de02 100644 --- a/app/logical/storage_manager/sftp.rb +++ b/app/logical/storage_manager/sftp.rb @@ -21,7 +21,7 @@ class StorageManager::SFTP < StorageManager temp_upload_path = dest_path + "-" + SecureRandom.uuid + ".tmp" dest_backup_path = dest_path + "-" + SecureRandom.uuid + ".bak" - each_host do |host, sftp| + each_host do |_host, sftp| sftp.upload!(file.path, temp_upload_path) sftp.setstat!(temp_upload_path, permissions: DEFAULT_PERMISSIONS) @@ -40,7 +40,7 @@ class StorageManager::SFTP < StorageManager end def delete(dest_path) - each_host do |host, sftp| + each_host do |_host, sftp| force { sftp.remove!(dest_path) } end end diff --git a/app/logical/tag_autocomplete.rb b/app/logical/tag_autocomplete.rb index 31f4c1566..ba34e658c 100644 --- a/app/logical/tag_autocomplete.rb +++ b/app/logical/tag_autocomplete.rb @@ -25,8 +25,7 @@ module TagAutocomplete def search(query) query = Tag.normalize_name(query) - candidates = count_sort( - query, + count_sort( search_exact(query, 8) + search_prefix(query, 4) + search_correct(query, 2) + @@ -34,7 +33,7 @@ module TagAutocomplete ) end - def count_sort(query, words) + def count_sort(words) words.uniq(&:name).sort_by do |x| x.post_count * x.weight end.reverse.slice(0, LIMIT) diff --git a/app/logical/tag_relationship_retirement_service.rb b/app/logical/tag_relationship_retirement_service.rb index e3ad7eda8..0aa79cc6b 100644 --- a/app/logical/tag_relationship_retirement_service.rb +++ b/app/logical/tag_relationship_retirement_service.rb @@ -4,19 +4,11 @@ module TagRelationshipRetirementService THRESHOLD = 2.years def forum_topic_title - return "Retired tag aliases & implications" + "Retired tag aliases & implications" end def forum_topic_body - return "This topic deals with tag relationships created two or more years ago that have not been used since. They will be retired. This topic will be updated as an automated system retires expired relationships." - end - - def dry_run - [TagAlias, TagImplication].each do |model| - each_candidate(model) do |rel| - puts "#{rel.relationship} #{rel.antecedent_name} -> #{rel.consequent_name} retired" - end - end + "This topic deals with tag relationships created two or more years ago that have not been used since. They will be retired. This topic will be updated as an automated system retires expired relationships." end def forum_topic @@ -27,7 +19,7 @@ module TagRelationshipRetirementService forum_post = ForumPost.create!(creator: User.system, body: forum_topic_body, topic: topic) end end - return topic + topic end def find_and_retire! @@ -50,16 +42,6 @@ module TagRelationshipRetirementService yield(rel) end end - - # model.active.where("created_at < ?", SMALL_THRESHOLD.ago).find_each do |rel| - # if is_underused?(rel.consequent_name) - # yield(rel) - # end - # end - end - - def is_underused?(name) - (Tag.find_by_name(name).try(:post_count) || 0) < COUNT_THRESHOLD end def is_unused?(name) diff --git a/app/logical/upload_limit.rb b/app/logical/upload_limit.rb index 014706210..752741d05 100644 --- a/app/logical/upload_limit.rb +++ b/app/logical/upload_limit.rb @@ -2,7 +2,7 @@ class UploadLimit extend Memoist INITIAL_POINTS = 1000 - MAXIMUM_POINTS = 10000 + MAXIMUM_POINTS = 10_000 attr_reader :user @@ -75,7 +75,7 @@ class UploadLimit points += upload_value(points, is_deleted) points = points.clamp(0, MAXIMUM_POINTS) - #warn "slots: %2d, points: %3d, value: %2d" % [UploadLimit.points_to_level(points) + 5, points, UploadLimit.upload_value(level, is_deleted)] + # warn "slots: %2d, points: %3d, value: %2d" % [UploadLimit.points_to_level(points) + 5, points, UploadLimit.upload_value(level, is_deleted)] end points diff --git a/app/logical/upload_service.rb b/app/logical/upload_service.rb index c2473284c..5a5d415b9 100644 --- a/app/logical/upload_service.rb +++ b/app/logical/upload_service.rb @@ -15,7 +15,6 @@ class UploadService start! end rescue ActiveRecord::RecordNotUnique - return end def start! @@ -31,8 +30,8 @@ class UploadService begin create_post_from_upload(@upload) - rescue Exception => x - @upload.update(status: "error: #{x.class} - #{x.message}", backtrace: x.backtrace.join("\n")) + rescue Exception => e + @upload.update(status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n")) end return @upload end @@ -53,16 +52,16 @@ class UploadService @upload.save! @post = create_post_from_upload(@upload) - return @upload - rescue Exception => x - @upload.update(status: "error: #{x.class} - #{x.message}", backtrace: x.backtrace.join("\n")) + @upload + rescue Exception => e + @upload.update(status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n")) @upload end end def warnings return [] if @post.nil? - return @post.warnings.full_messages + @post.warnings.full_messages end def create_post_from_upload(upload) diff --git a/app/logical/upload_service/controller_helper.rb b/app/logical/upload_service/controller_helper.rb index d311ca19b..378ca07f3 100644 --- a/app/logical/upload_service/controller_helper.rb +++ b/app/logical/upload_service/controller_helper.rb @@ -7,11 +7,8 @@ class UploadService # this gets called from UploadsController#new so we need to preprocess async UploadPreprocessorDelayedStartJob.perform_later(url, ref, CurrentUser.user) - begin - download = Downloads::File.new(url, ref) - remote_size = download.size - rescue Exception - end + strategy = Sources::Strategies.find(url, ref) + remote_size = strategy.remote_size return [upload, remote_size] end @@ -21,7 +18,7 @@ class UploadService Preprocessor.new(file: file).delayed_start(CurrentUser.id) end - return [upload] + [upload] end end end diff --git a/app/logical/upload_service/preprocessor.rb b/app/logical/upload_service/preprocessor.rb index a0b411818..042c50cc6 100644 --- a/app/logical/upload_service/preprocessor.rb +++ b/app/logical/upload_service/preprocessor.rb @@ -46,11 +46,9 @@ class UploadService def predecessor if md5.present? - return Upload.where(status: ["preprocessed", "preprocessing"], md5: md5).first - end - - if Utils.is_downloadable?(source) - return Upload.where(status: ["preprocessed", "preprocessing"], source: source).first + Upload.where(status: ["preprocessed", "preprocessing"], md5: md5).first + elsif Utils.is_downloadable?(source) + Upload.where(status: ["preprocessed", "preprocessing"], source: source).first end end @@ -63,21 +61,20 @@ class UploadService start! end rescue ActiveRecord::RecordNotUnique - return end def start! if Utils.is_downloadable?(source) if Post.system_tag_match("source:#{canonical_source}").where.not(id: original_post_id).exists? - raise ActiveRecord::RecordNotUnique.new("A post with source #{canonical_source} already exists") + raise ActiveRecord::RecordNotUnique, "A post with source #{canonical_source} already exists" end if Upload.where(source: source, status: "completed").exists? - raise ActiveRecord::RecordNotUnique.new("A completed upload with source #{source} already exists") + raise ActiveRecord::RecordNotUnique, "A completed upload with source #{source} already exists" end if Upload.where(source: source).where("status like ?", "error%").exists? - raise ActiveRecord::RecordNotUnique.new("An errored upload with source #{source} already exists") + raise ActiveRecord::RecordNotUnique, "An errored upload with source #{source} already exists" end end @@ -95,21 +92,21 @@ class UploadService upload.tag_string = params[:tag_string] upload.status = "preprocessed" upload.save! - rescue Exception => x - upload.update(file_ext: nil, status: "error: #{x.class} - #{x.message}", backtrace: x.backtrace.join("\n")) + rescue Exception => e + upload.update(file_ext: nil, status: "error: #{e.class} - #{e.message}", backtrace: e.backtrace.join("\n")) end - return upload + upload end def finish!(upload = nil) - pred = upload || self.predecessor + pred = upload || predecessor # regardless of who initialized the upload, credit should # goto whoever submitted the form pred.initialize_attributes - pred.attributes = self.params + pred.attributes = params # if a file was uploaded after the preprocessing occurred, # then process the file and overwrite whatever the preprocessor @@ -118,7 +115,7 @@ class UploadService pred.status = "completed" pred.save - return pred + pred end end end diff --git a/app/logical/upload_service/replacer.rb b/app/logical/upload_service/replacer.rb index b1ac345d6..f64198c5a 100644 --- a/app/logical/upload_service/replacer.rb +++ b/app/logical/upload_service/replacer.rb @@ -62,7 +62,7 @@ class UploadService end def source_strategy(upload) - return Sources::Strategies.find(upload.source, upload.referer_url) + Sources::Strategies.find(upload.source, upload.referer_url) end def find_replacement_url(repl, upload) @@ -78,7 +78,7 @@ class UploadService return source_strategy(upload).canonical_url end - return upload.source + upload.source end def process! diff --git a/app/logical/upload_service/utils.rb b/app/logical/upload_service/utils.rb index 09c2e47e6..e4316cd90 100644 --- a/app/logical/upload_service/utils.rb +++ b/app/logical/upload_service/utils.rb @@ -71,19 +71,19 @@ class UploadService return file if file.present? raise "No file or source URL provided" if upload.source_url.blank? - download = Downloads::File.new(upload.source_url, upload.referer_url) - file, strategy = download.download! + strategy = Sources::Strategies.find(upload.source_url, upload.referer_url) + file = strategy.download_file! - if download.data[:ugoira_frame_data].present? + if strategy.data[:ugoira_frame_data].present? upload.context = { "ugoira" => { - "frame_data" => download.data[:ugoira_frame_data], + "frame_data" => strategy.data[:ugoira_frame_data], "content_type" => "image/jpeg" } } end - return file + file end end end diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb index b87e6297f..8577f19a1 100644 --- a/app/logical/user_deletion.rb +++ b/app/logical/user_deletion.rb @@ -2,6 +2,7 @@ class UserDeletion include ActiveModel::Validations attr_reader :user, :password + validate :validate_deletion def initialize(user, password) diff --git a/app/logical/user_promotion.rb b/app/logical/user_promotion.rb index 8f46ba0f0..b3b7a32bb 100644 --- a/app/logical/user_promotion.rb +++ b/app/logical/user_promotion.rb @@ -88,11 +88,7 @@ class UserPromotion end def create_dmail - Dmail.create_automated( - :to_id => user.id, - :title => "You have been promoted", - :body => build_messages - ) + Dmail.create_automated(to_id: user.id, title: "Your account has been updated", body: build_messages) end def create_user_feedback diff --git a/app/logical/validating_socket.rb b/app/logical/validating_socket.rb new file mode 100644 index 000000000..446609073 --- /dev/null +++ b/app/logical/validating_socket.rb @@ -0,0 +1,27 @@ +# A TCPSocket wrapper that disallows connections to local or private IPs. Used for SSRF protection. +# https://owasp.org/www-community/attacks/Server_Side_Request_Forgery + +require "resolv" + +class ValidatingSocket < TCPSocket + class ProhibitedIpError < StandardError; end + + def initialize(hostname, port) + ip = validate_hostname!(hostname) + super(ip, port) + end + + def validate_hostname!(hostname) + ip = IPAddress.parse(::Resolv.getaddress(hostname)) + raise ProhibitedIpError, "Connection to #{hostname} failed; #{ip} is a prohibited IP" if prohibited_ip?(ip) + ip.to_s + end + + def prohibited_ip?(ip) + if ip.ipv4? + ip.loopback? || ip.link_local? || ip.multicast? || ip.private? + elsif ip.ipv6? + ip.loopback? || ip.link_local? || ip.unique_local? || ip.unspecified? + end + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index ee9fa44a0..ccd96df91 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -16,7 +16,8 @@ class ApplicationRecord < ActiveRecord::Base search_params = params.fetch(:search, {}).permit! search_params = defaults.merge(search_params).with_indifferent_access - search(search_params).paginate(params[:page], limit: params[:limit], search_count: count_pages) + max_limit = (params[:format] == "sitemap") ? 10_000 : 1_000 + search(search_params).paginate(params[:page], limit: params[:limit], max_limit: max_limit, search_count: count_pages) end end end diff --git a/app/models/artist.rb b/app/models/artist.rb index 6f7675d4d..e40820fef 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -8,10 +8,12 @@ class Artist < ApplicationRecord before_validation :normalize_name before_validation :normalize_other_names - after_save :create_version - after_save :clear_url_string_changed validate :validate_tag_category validates :name, tag_name: true, uniqueness: true + before_save :update_tag_category + after_save :create_version + after_save :clear_url_string_changed + has_many :members, :class_name => "Artist", :foreign_key => "group_name", :primary_key => "name" has_many :urls, :dependent => :destroy, :class_name => "ArtistUrl", :autosave => true has_many :versions, -> {order("artist_versions.id ASC")}, :class_name => "ArtistVersion" @@ -151,14 +153,20 @@ class Artist < ApplicationRecord module TagMethods def validate_tag_category - return unless !is_deleted? && name_changed? + return unless !is_deleted? && name_changed? && tag.present? - if tag.category_name == "General" - tag.update(category: Tag.categories.artist) - elsif tag.category_name != "Artist" + 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" end end + + def update_tag_category + return unless !is_deleted? && name_changed? && tag.present? + + if tag.category_name != "Artist" && tag.empty? + tag.update!(category: Tag.categories.artist) + end + end end module BanMethods diff --git a/app/models/artist_url.rb b/app/models/artist_url.rb index b56c8ead8..b4e4d9af5 100644 --- a/app/models/artist_url.rb +++ b/app/models/artist_url.rb @@ -20,20 +20,19 @@ class ArtistUrl < ApplicationRecord nil else url = url.sub(%r!^https://!, "http://") - url = url.sub(%r!^http://([^/]+)!i) { |domain| domain.downcase } url = url.sub(%r!^http://blog\d+\.fc2!, "http://blog.fc2") url = url.sub(%r!^http://blog-imgs-\d+\.fc2!, "http://blog.fc2") url = url.sub(%r!^http://blog-imgs-\d+-\w+\.fc2!, "http://blog.fc2") - # url = url.sub(%r!^(http://seiga.nicovideo.jp/user/illust/\d+)\?.+!, '\1/') url = url.sub(%r!^http://pictures.hentai-foundry.com//!, "http://pictures.hentai-foundry.com/") - # XXX should be handled by pixiv strategy. - url = url.sub(%r!\Ahttps?://www\.pixiv\.net/(?:en/)?users/(\d+)\z!i, 'https://www.pixiv.net/member.php?id=\1') - # the strategy won't always work for twitter because it looks for a status url = url.downcase if url =~ %r!^https?://(?:mobile\.)?twitter\.com! url = Sources::Strategies.find(url).normalize_for_artist_finder + + # XXX the Pixiv strategy should implement normalize_for_artist_finder and return the correct url directly. + url = url.sub(%r!\Ahttps?://www\.pixiv\.net/(?:en/)?users/(\d+)\z!i, 'https://www.pixiv.net/member.php?id=\1') + url = url.gsub(/\/+\Z/, "") url = url.gsub(%r!^https://!, "http://") url + "/" @@ -96,7 +95,15 @@ class ArtistUrl < ApplicationRecord end def normalize + # Perform some normalization with Addressable on the URL itself + # - Converts scheme and hostname to downcase + # - Converts unicode hostname to Punycode + uri = Addressable::URI.parse(url) + uri.site = uri.normalized_site + self.url = uri.to_s self.normalized_url = self.class.normalize(url) + rescue Addressable::URI::InvalidURIError + # Don't bother normalizing the URL if there is errors end def initialize_normalized_url @@ -111,9 +118,18 @@ class ArtistUrl < ApplicationRecord end end + def validate_scheme(uri) + errors[:url] << "'#{uri}' must begin with http:// or https:// " unless uri.scheme.in?(%w[http https]) + end + + def validate_hostname(uri) + errors[:url] << "'#{uri}' has a hostname '#{uri.host}' that does not contain a dot" unless uri.host&.include?('.') + end + def validate_url_format uri = Addressable::URI.parse(url) - errors[:url] << "'#{uri}' must begin with http:// or https:// " if !uri.scheme.in?(%w[http https]) + validate_scheme(uri) + validate_hostname(uri) rescue Addressable::URI::InvalidURIError => error errors[:url] << "'#{uri}' is malformed: #{error}" end diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb index 17083458b..b45a6d86c 100644 --- a/app/models/moderation_report.rb +++ b/app/models/moderation_report.rb @@ -34,7 +34,7 @@ class ModerationReport < ApplicationRecord def forum_topic topic = ForumTopic.find_by_title(forum_topic_title) if topic.nil? - CurrentUser.as_system do + CurrentUser.scoped(User.system) do topic = ForumTopic.create!(creator: User.system, title: forum_topic_title, category_id: 0, min_level: User::Levels::MODERATOR) forum_post = ForumPost.create!(creator: User.system, body: forum_topic_body, topic: topic) end diff --git a/app/models/post.rb b/app/models/post.rb index e1d40ef91..1d2121e50 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -300,7 +300,7 @@ class Post < ApplicationRecord end def autoban - if has_tag?("banned_artist") + if has_tag?("banned_artist") || has_tag?("paid_reward") self.is_banned = true end end @@ -499,7 +499,7 @@ class Post < ApplicationRecord end def add_automatic_tags(tags) - tags -= %w(incredibly_absurdres absurdres highres lowres huge_filesize flash webm mp4) + tags -= %w(incredibly_absurdres absurdres highres lowres huge_filesize flash) if has_dimensions? if image_width >= 10_000 || image_height >= 10_000 @@ -532,12 +532,8 @@ class Post < ApplicationRecord tags << "flash" end - if is_webm? - tags << "webm" - end - - if is_mp4? - tags << "mp4" + if is_video? + tags << "video" end if is_ugoira? @@ -625,10 +621,10 @@ class Post < ApplicationRecord add_pool!(pool) if pool when /^fav:(.+)$/i - add_favorite!(CurrentUser.user) + add_favorite(CurrentUser.user) when /^-fav:(.+)$/i - remove_favorite!(CurrentUser.user) + remove_favorite(CurrentUser.user) when /^(up|down)vote:(.+)$/i vote!($1) @@ -792,6 +788,13 @@ class Post < ApplicationRecord rescue PostVote::Error end + def remove_favorite(user) + remove_favorite!(user) + true + rescue Favorite::Error + false + end + # users who favorited this post, ordered by users who favorited it first def favorited_users favorited_user_ids = fav_string.scan(/\d+/).map(&:to_i) @@ -1467,7 +1470,8 @@ class Post < ApplicationRecord end def banblocked?(user = CurrentUser.user) - is_banned? && !user.is_gold? + return false unless is_banned? + (has_tag?("paid_reward") && !user.is_approver?) || !user.is_gold? end def visible?(user = CurrentUser.user) diff --git a/app/models/post_disapproval.rb b/app/models/post_disapproval.rb index 6e594c4cb..bf00344fe 100644 --- a/app/models/post_disapproval.rb +++ b/app/models/post_disapproval.rb @@ -18,24 +18,6 @@ class PostDisapproval < ApplicationRecord PostDisapproval.where("post_id in (select _.post_id from post_disapprovals _ where _.created_at < ?)", DELETION_THRESHOLD.ago).delete_all end - def self.dmail_messages! - disapprovals = PostDisapproval.with_message.where("created_at >= ?", 1.day.ago).group_by do |pd| - pd.post.uploader - end - - disapprovals.each do |uploader, list| - message = list.map do |x| - "* post ##{x.post_id}: #{x.message}" - end.join("\n") - - Dmail.create_automated( - :to_id => uploader.id, - :title => "Someone has commented on your uploads", - :body => message - ) - end - end - concerning :SearchMethods do class_methods do def search(params) diff --git a/app/models/post_version.rb b/app/models/post_version.rb index a6be23df2..a38ee2779 100644 --- a/app/models/post_version.rb +++ b/app/models/post_version.rb @@ -33,7 +33,7 @@ class PostVersion < ApplicationRecord end def tag_matches(string) - tag = string.split(/\S+/)[0] + tag = string.match(/\S+/)[0] return all if tag.nil? tag = "*#{tag}*" unless tag =~ /\*/ where_ilike(:tags, tag) diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb index 005f7dfa6..d992bf11d 100644 --- a/app/models/saved_search.rb +++ b/app/models/saved_search.rb @@ -18,7 +18,7 @@ class SavedSearch < ApplicationRecord post_ids = Set.new queries.each do |query| redis_key = "search:#{query}" - if redis.exists(redis_key) + if redis.exists?(redis_key) sub_ids = redis.smembers(redis_key).map(&:to_i) post_ids.merge(sub_ids) else @@ -115,7 +115,7 @@ class SavedSearch < ApplicationRecord def populate(query, timeout: 10_000) redis_key = "search:#{query}" - return if redis.exists(redis_key) + return if redis.exists?(redis_key) post_ids = Post.with_timeout(timeout, [], query: query) do Post.system_tag_match(query).limit(QUERY_LIMIT).pluck(:id) diff --git a/app/models/upload.rb b/app/models/upload.rb index e7b2651fb..3d46380b7 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -75,6 +75,7 @@ class Upload < ApplicationRecord scope :pending, -> { where(status: "pending") } scope :preprocessed, -> { where(status: "preprocessed") } + scope :completed, -> { where(status: "completed") } scope :uploaded_by, ->(user_id) { where(uploader_id: user_id) } def initialize_attributes @@ -83,17 +84,19 @@ class Upload < ApplicationRecord self.server = Socket.gethostname end - def self.prune!(date = 1.day.ago) - where("created_at < ?", date).lock.destroy_all + def self.prune! + completed.where("created_at < ?", 1.hour.ago).lock.destroy_all + preprocessed.where("created_at < ?", 1.day.ago).lock.destroy_all + where("created_at < ?", 3.days.ago).lock.destroy_all end def self.visible(user) if user.is_admin? all elsif user.is_member? - where(uploader: user) + completed.or(where(uploader: user)) else - none + completed end end @@ -108,7 +111,7 @@ class Upload < ApplicationRecord return end - DanbooruLogger.info("Uploads: Deleting files for upload md5=#{md5}", upload: as_json) + DanbooruLogger.info("Uploads: Deleting files for upload md5=#{md5}") Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :original) Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :large) Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :preview) diff --git a/app/models/user.rb b/app/models/user.rb index 064b8a6c3..89ab85e6e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -527,7 +527,7 @@ class User < ApplicationRecord q = q.where("level <= ?", params[:max_level].to_i) end - %w[can_approve_posts can_upload_free].each do |flag| + %w[can_approve_posts can_upload_free is_banned].each do |flag| if params[flag].to_s.truthy? q = q.bit_prefs_match(flag, true) elsif params[flag].to_s.falsy? diff --git a/app/models/user_name_change_request.rb b/app/models/user_name_change_request.rb index e80adcc82..2cdafe5a6 100644 --- a/app/models/user_name_change_request.rb +++ b/app/models/user_name_change_request.rb @@ -18,6 +18,12 @@ class UserNameChangeRequest < ApplicationRecord end end + def self.search(params) + q = super + q = q.search_attributes(params, :user, :original_name, :desired_name) + q.apply_default_order(params) + end + def update_name! user.update!(name: desired_name) end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index efef59f3a..73f7f8ced 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -53,7 +53,11 @@ class WikiPage < ApplicationRecord end def linked_to(title) - where(id: DtextLink.wiki_page.wiki_link.where(link_target: title).select(:model_id)) + where(dtext_links: DtextLink.wiki_page.wiki_link.where(link_target: normalize_title(title))) + end + + def not_linked_to(title) + where.not(dtext_links: DtextLink.wiki_page.wiki_link.where(link_target: normalize_title(title))) end def default_order @@ -82,6 +86,10 @@ class WikiPage < ApplicationRecord q = q.linked_to(params[:linked_to]) end + if params[:not_linked_to].present? + q = q.not_linked_to(params[:not_linked_to]) + end + if params[:hide_deleted].to_s.truthy? q = q.where("is_deleted = false") end @@ -146,6 +154,7 @@ class WikiPage < ApplicationRecord end def self.normalize_title(title) + return if title.blank? title.downcase.delete_prefix("~").gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "") end diff --git a/app/policies/forum_post_policy.rb b/app/policies/forum_post_policy.rb index 14e315e9c..546991f09 100644 --- a/app/policies/forum_post_policy.rb +++ b/app/policies/forum_post_policy.rb @@ -24,7 +24,7 @@ class ForumPostPolicy < ApplicationPolicy end def votable? - unbanned? && show? && record.bulk_update_request.present? && record.bulk_update_request.is_pending? + unbanned? && show? && record.bulk_update_request.present? && record.bulk_update_request.is_pending? && record.bulk_update_request.user_id != user.id end def reportable? diff --git a/app/policies/post_policy.rb b/app/policies/post_policy.rb index fa94e340a..413ce7876 100644 --- a/app/policies/post_policy.rb +++ b/app/policies/post_policy.rb @@ -47,10 +47,6 @@ class PostPolicy < ApplicationPolicy record.visible?(user) end - def can_view_uploader? - user.is_approver? - end - def can_lock_rating? user.is_builder? end @@ -91,7 +87,7 @@ class PostPolicy < ApplicationPolicy attributes += [:has_large, :has_visible_children, :is_favorited?] attributes += TagCategory.categories.map {|x| "tag_string_#{x}".to_sym} attributes += [:file_url, :large_file_url, :preview_file_url] if visible? - attributes -= [:md5, :file_ext] if !visible? + attributes -= [:id, :md5, :file_ext] if !visible? attributes -= [:fav_string] if !user.is_moderator? attributes end diff --git a/app/policies/upload_policy.rb b/app/policies/upload_policy.rb index 66b5d5ff1..a5c7f7211 100644 --- a/app/policies/upload_policy.rb +++ b/app/policies/upload_policy.rb @@ -1,6 +1,6 @@ class UploadPolicy < ApplicationPolicy def show? - user.is_admin? || record.uploader_id == user.id + record.is_completed? || user.is_admin? || record.uploader_id == user.id end def batch? @@ -15,9 +15,19 @@ class UploadPolicy < ApplicationPolicy unbanned? end + def can_view_tags? + user.is_admin? || record.uploader_id == user.id + end + def permitted_attributes %i[file source tag_string rating status parent_id artist_commentary_title artist_commentary_desc referer_url md5_confirmation as_pending translated_commentary_title translated_commentary_desc] end + + def api_attributes + attributes = super + attributes -= [:tag_string] unless can_view_tags? + attributes + end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 2814fde68..0c9c0469c 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,6 +1,6 @@ class UserPolicy < ApplicationPolicy def create? - !sockpuppet? + user.is_anonymous? && !sockpuppet? end def update? diff --git a/app/presenters/tag_set_presenter.rb b/app/presenters/tag_set_presenter.rb index 605f08020..6182b2073 100644 --- a/app/presenters/tag_set_presenter.rb +++ b/app/presenters/tag_set_presenter.rb @@ -125,8 +125,7 @@ class TagSetPresenter end humanized_tag = humanize_tags ? name.tr("_", " ") : name - itemprop = 'itemprop="author"' if tag.artist? - html << %{<a class="search-tag" #{itemprop} href="/posts?tags=#{u(name)}">#{h(humanized_tag)}</a> } + html << %{<a class="search-tag" href="/posts?tags=#{u(name)}">#{h(humanized_tag)}</a> } unless name_only || tag.new_record? if count >= 10_000 diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index 3a3add269..1b63c7989 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -56,15 +56,15 @@ class UserPresenter end def upload_count(template) - template.link_to(user.post_upload_count, template.posts_path(:tags => "user:#{user.name}")) + template.link_to(user.post_upload_count, template.posts_path(tags: "user:#{user.name}"), rel: "nofollow") end def deleted_upload_count(template) - template.link_to(user.posts.deleted.count, template.posts_path(:tags => "status:deleted user:#{user.name}")) + template.link_to(user.posts.deleted.count, template.posts_path(tags: "status:deleted user:#{user.name}"), rel: "nofollow") end def favorite_count(template) - template.link_to(user.favorite_count, template.posts_path(tags: "ordfav:#{user.name}")) + template.link_to(user.favorite_count, template.posts_path(tags: "ordfav:#{user.name}"), rel: "nofollow") end def favorite_group_count(template) @@ -78,7 +78,7 @@ class UserPresenter def commented_posts_count(template) count = PostQueryBuilder.new("commenter:#{user.name}").fast_count count = "?" if count.nil? - template.link_to(count, template.posts_path(:tags => "commenter:#{user.name} order:comment_bumped")) + template.link_to(count, template.posts_path(tags: "commenter:#{user.name} order:comment_bumped"), rel: "nofollow") end def post_version_count(template) @@ -92,7 +92,7 @@ class UserPresenter def noted_posts_count(template) count = PostQueryBuilder.new("noteupdater:#{user.name}").fast_count count = "?" if count.nil? - template.link_to(count, template.posts_path(:tags => "noteupdater:#{user.name} order:note")) + template.link_to(count, template.posts_path(tags: "noteupdater:#{user.name} order:note"), rel: "nofollow") end def wiki_page_version_count(template) @@ -108,7 +108,7 @@ class UserPresenter end def forum_post_count(template) - template.link_to(user.forum_post_count, template.forum_posts_path(:search => {:creator_id => user.id})) + template.link_to(user.forum_post_count, template.forum_posts_path(search: { creator_id: user.id }), rel: "nofollow") end def pool_version_count(template) @@ -128,7 +128,7 @@ class UserPresenter end def approval_count(template) - template.link_to(Post.where("approver_id = ?", user.id).count, template.posts_path(:tags => "approver:#{user.name}")) + template.link_to(Post.where(approver: user).count, template.posts_path(tags: "approver:#{user.name}"), rel: "nofollow") end def feedbacks(template) diff --git a/app/views/application/index.sitemap.erb b/app/views/application/index.sitemap.erb new file mode 100644 index 000000000..a7ebe75fa --- /dev/null +++ b/app/views/application/index.sitemap.erb @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <% @current_item.each do |item| %> + <url> + <loc><%= polymorphic_url(item) %></loc> + <lastmod><%= item.updated_at.iso8601 %></lastmod> + </url> + <% end %> +</urlset> diff --git a/app/views/artist_versions/index.html.erb b/app/views/artist_versions/index.html.erb index 9c4d4fe06..0e8559b0f 100644 --- a/app/views/artist_versions/index.html.erb +++ b/app/views/artist_versions/index.html.erb @@ -6,7 +6,7 @@ <%= render "listing" %> - <%= numbered_paginator(@artist_versions, :search_count => params[:search]) %> + <%= numbered_paginator(@artist_versions) %> </div> </div> diff --git a/app/views/artists/_secondary_links.html.erb b/app/views/artists/_secondary_links.html.erb index 8093e374e..c09aa9280 100644 --- a/app/views/artists/_secondary_links.html.erb +++ b/app/views/artists/_secondary_links.html.erb @@ -15,7 +15,7 @@ <%= subnav_link_to "Edit", edit_artist_path(@artist), :"data-shortcut" => "e" %> <% end %> <%= subnav_link_to "History", artist_versions_path(:search => {:artist_id => @artist.id}) %> - <% if policy(@artist).update? %> + <% if current_page?(action: "edit") && policy(@artist).update? %> <% if @artist.is_deleted? %> <%= subnav_link_to "Undelete", artist_path(@artist, format: "js"), method: :put, data: {confirm: "Are you sure you want to undelete this artist?", params: "artist[is_deleted]=false"}, remote: true %> <% else %> diff --git a/app/views/artists/_show.html.erb b/app/views/artists/_show.html.erb index 3e7a65bec..8a00e1bf6 100644 --- a/app/views/artists/_show.html.erb +++ b/app/views/artists/_show.html.erb @@ -8,12 +8,14 @@ <% if @artist.is_banned? && !policy(@artist).can_view_banned? %> <p>The artist requested removal of this page.</p> <% else %> - <% if @artist.wiki_page.present? %> - <div class="prose"> - <%= format_text(@artist.wiki_page.body, :disable_mentions => true) %> - </div> + <% if @artist.wiki_page.present? && !@artist.wiki_page.is_deleted? %> + <div class="artist-wiki"> + <div class="prose"> + <%= format_text(@artist.wiki_page.body, :disable_mentions => true) %> + </div> - <p><%= link_to "View wiki page", @artist.wiki_page %></p> + <p><%= link_to "View wiki page", @artist.wiki_page %></p> + </div> <% end %> <%= yield %> diff --git a/app/views/artists/index.html.erb b/app/views/artists/index.html.erb index 49f2577e1..ed8edf733 100644 --- a/app/views/artists/index.html.erb +++ b/app/views/artists/index.html.erb @@ -14,7 +14,7 @@ <% end %> <% t.column "Other Names", td: {class: "col-expand"} do |artist| %> <% artist.other_names.each do |name| %> - <%= link_to name, artists_path(search: { any_name_matches: name }), class: "artist-other-name" %> + <%= link_to name, artists_path(search: { any_name_matches: name }), class: "artist-other-name", rel: "nofollow" %> <% end %> <% end %> <% t.column "Status" do |artist| %> @@ -32,12 +32,6 @@ <% t.column column: "control" do |artist| %> <% if policy(artist).update? %> <%= link_to "Edit", edit_artist_path(artist) %> - - <% if artist.is_deleted? %> - | <%= link_to "Undelete", artist_path(artist, artist: { is_deleted: false }), method: :put, remote: true %> - <% else %> - | <%= link_to "Delete", artist_path(artist, artist: { is_deleted: true }), method: :put, remote: true %> - <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/artists/show_or_new.html.erb b/app/views/artists/show_or_new.html.erb index 05b1f0479..73e8265e3 100644 --- a/app/views/artists/show_or_new.html.erb +++ b/app/views/artists/show_or_new.html.erb @@ -1,5 +1,5 @@ <%= render layout: "show" do %> <div> - <p>This artist entry does not exist. <%= link_to "Create new artist entry", new_artist_path(artist: { name: params[:name] }) %>.</p> + <p>This artist entry does not exist. <%= link_to "Create new artist entry", new_artist_path(artist: { name: params[:name] }), rel: "nofollow" %>.</p> </div> <% end %> diff --git a/app/views/bulk_update_requests/_form.html.erb b/app/views/bulk_update_requests/_form.html.erb index 8d48d16ea..acf886b59 100644 --- a/app/views/bulk_update_requests/_form.html.erb +++ b/app/views/bulk_update_requests/_form.html.erb @@ -24,7 +24,7 @@ <% if @bulk_update_request.new_record? %> <div class="input"> - <%= dtext_field "bulk_update_request", "reason", :name => "Reason" %> + <%= f.input :reason, as: :dtext %> </div> <% end %> @@ -44,5 +44,5 @@ <% end %> <%= f.submit value: "Submit" %> - <%= dtext_preview_button "bulk_update_request", "reason" %> + <%= dtext_preview_button "bulk_update_request_reason" %> <% end %> diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index fbf255f5e..0b643d6b0 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -1,12 +1,12 @@ <%= error_messages_for :comment %> -<%= edit_form_for(comment, html: { style: ("display: none;" if local_assigns[:hidden]), class: "edit_comment" }) do |f| %> +<%= edit_form_for(comment, namespace: "post_#{comment&.post_id}_comment_#{comment.id || "new"}", html: { style: ("display: none;" if local_assigns[:hidden]), class: "edit_comment" }) do |f| %> <% if comment.new_record? %> <%= f.hidden_field :post_id %> <% end %> - <%= dtext_field "comment", "body", classes: "autocomplete-mentions", value: comment.body, input_id: "comment_body_for_#{comment.id}", preview_id: "dtext-preview-for-#{comment.id}", hint: link_to_wiki("Comment rules", "howto:comment").html_safe %> + <%= f.input :body, as: :dtext %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "comment", "body", :input_id => "comment_body_for_#{comment.id}", :preview_id => "dtext-preview-for-#{comment.id}" %> + <%= dtext_preview_button "comment_body" %> <% if comment.new_record? %> <%= f.input :do_not_bump_post, :label => "No bump" %> <% end %> diff --git a/app/views/comments/partials/index/_header.html.erb b/app/views/comments/partials/index/_header.html.erb index 86211bf0b..da2bb9463 100644 --- a/app/views/comments/partials/index/_header.html.erb +++ b/app/views/comments/partials/index/_header.html.erb @@ -4,12 +4,10 @@ <strong>Date</strong> <%= compact_time(post.created_at) %> </span> - <% if policy(post).can_view_uploader? %> - <span class="info"> - <strong>User</strong> - <%= link_to_user(post.uploader) %> - </span> - <% end %> + <span class="info"> + <strong>Uploader</strong> + <%= link_to_user(post.uploader) %> + </span> <span class="info"> <strong>Rating</strong> <%= post.pretty_rating %> diff --git a/app/views/dmails/_form.html.erb b/app/views/dmails/_form.html.erb index f3efb498c..2a5b2925d 100644 --- a/app/views/dmails/_form.html.erb +++ b/app/views/dmails/_form.html.erb @@ -1,7 +1,7 @@ <%= edit_form_for(dmail) do |f| %> <%= f.input :to_name, :label => "To", :input_html => { value: dmail.to.try(:name), data: { autocomplete: "user" } } %> <%= f.input :title, :as => :string %> - <%= dtext_field "dmail", "body" %> + <%= f.input :body, as: :dtext %> <%= f.button :submit, "Send", :data => { :disable_with => "Sending..." } %> - <%= dtext_preview_button "dmail", "body" %> + <%= dtext_preview_button "dmail_body" %> <% end %> diff --git a/app/views/dtext/_form.html.erb b/app/views/dtext/_form.html.erb deleted file mode 100644 index ef79e084e..000000000 --- a/app/views/dtext/_form.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<%# name, input_id, input_name, preview_id, value, type %> -<div class="input text optional <%= classes %>"> - <label class="text optional" for="<%= input_id %>"><%= name %></label> - - <div class="dtext-previewable"> - <% if type == "text" %> - <textarea id="<%= input_id %>" class="text optional" rows="20" name="<%= input_name %>" cols="30"><%= value %></textarea> - <% else %> - <input type="text" id="<%= input_id %>" class="text optional" name="<%= input_name %>" value="<%= value %>"> - <% end %> - <div id="<%= preview_id %>" class="dtext-preview prose"></div> - </div> - <span class="hint"> - <%= link_to "Formatting help", dtext_help_path, remote: true, method: :get %>. - <% if hint.present? %> - <%= hint %>. - <% end %> - </span> -</div> diff --git a/app/views/explore/posts/missed_searches.html.erb b/app/views/explore/posts/missed_searches.html.erb index 2575d1694..329b66c43 100644 --- a/app/views/explore/posts/missed_searches.html.erb +++ b/app/views/explore/posts/missed_searches.html.erb @@ -15,7 +15,7 @@ </tr> </thead> <tbody> - <% @search_service.each_search do |tags, count| %> + <% @missed_searches.each do |tags, count| %> <tr class="tag-type-<%= Tag.category_for(tags) %>"> <td><%= link_to tags, posts_path(:tags => tags) %></td> <td> diff --git a/app/views/explore/posts/searches.html.erb b/app/views/explore/posts/searches.html.erb index 42770ad5a..5256427eb 100644 --- a/app/views/explore/posts/searches.html.erb +++ b/app/views/explore/posts/searches.html.erb @@ -3,7 +3,7 @@ <div id="c-explore-posts"> <div id="a-searches"> - <h1>Popular Searches - <%= @search_service.date %></h1> + <h1>Popular Searches - <%= @date %></h1> <table class="striped" width="100%"> <thead> @@ -13,7 +13,7 @@ </tr> </thead> <tbody> - <% @search_service.each_search do |tags, count| %> + <% @searches.each do |tags, count| %> <tr class="tag-type-<%= Tag.category_for(tags) %>"> <td><%= link_to tags, posts_path(:tags => tags) %></td> <td style="text-align: right;"><%= count.to_i %></td> diff --git a/app/views/forum_posts/partials/edit/_form.html.erb b/app/views/forum_posts/partials/edit/_form.html.erb index 617219d23..9dfd75b26 100644 --- a/app/views/forum_posts/partials/edit/_form.html.erb +++ b/app/views/forum_posts/partials/edit/_form.html.erb @@ -1,8 +1,8 @@ <%= error_messages_for("forum_post") %> -<%= edit_form_for(forum_post) do |f| %> - <%= dtext_field "forum_post", "body", :value => forum_post.body, :classes => "autocomplete-mentions", :input_id => "forum_post_body_for_#{forum_post.id}", :preview_id => "dtext-preview-for-#{forum_post.id}" %> +<%= edit_form_for(forum_post, namespace: "forum_post_#{forum_post.id}") do |f| %> + <%= f.input :body, as: :dtext %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "forum_post", "body", :input_id => "forum_post_body_for_#{forum_post.id}", :preview_id => "dtext-preview-for-#{forum_post.id}" %> + <%= dtext_preview_button "forum_post_body" %> <% end %> diff --git a/app/views/forum_posts/partials/new/_form.html.erb b/app/views/forum_posts/partials/new/_form.html.erb index 622d01056..cbfa63350 100644 --- a/app/views/forum_posts/partials/new/_form.html.erb +++ b/app/views/forum_posts/partials/new/_form.html.erb @@ -6,8 +6,8 @@ <% else %> <%= f.input :topic_id, :label => "Topic ID" %> <% end %> - <%= dtext_field "forum_post", "body", :classes => "autocomplete-mentions" %> + <%= f.input :body, as: :dtext %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "forum_post", "body" %> + <%= dtext_preview_button "forum_post_body" %> <% end %> diff --git a/app/views/forum_topics/_form.html.erb b/app/views/forum_topics/_form.html.erb index f4e78c493..468befdca 100644 --- a/app/views/forum_topics/_form.html.erb +++ b/app/views/forum_topics/_form.html.erb @@ -10,7 +10,7 @@ </div> <%= f.simple_fields_for :original_post do |pf| %> - <%= dtext_field "forum_post", "body", :classes => "autocomplete-mentions", :input_name => "forum_topic[original_post_attributes][body]", :value => forum_topic.original_post.body, :input_id => "forum_post_body_for_#{forum_topic.original_post.id}", :preview_id => "dtext-preview-for-#{forum_topic.original_post.id}" %> + <%= pf.input :body, as: :dtext %> <% end %> <% if policy(forum_topic).moderate? %> @@ -20,6 +20,6 @@ <% end %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "forum_post", "body", :input_id => "forum_post_body_for_#{forum_topic.original_post.id}", :preview_id => "dtext-preview-for-#{forum_topic.original_post.id}" %> + <%= dtext_preview_button "forum_topic_original_post_body" %> <% end %> </div> diff --git a/app/views/layouts/blank.html.erb b/app/views/layouts/blank.html.erb index 955d07f4d..228dd0855 100644 --- a/app/views/layouts/blank.html.erb +++ b/app/views/layouts/blank.html.erb @@ -1,12 +1,12 @@ <!doctype html> -<html> +<html lang="en"> <head> + <meta charset="utf-8"> <title><%= page_title %> <%= csrf_meta_tag %> <%= raw Danbooru.config.custom_html_header_content %> - <%= javascript_pack_tag "application" %> <%= stylesheet_pack_tag "application" %> <%= yield :html_header %> diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index bf1a94442..e389762f9 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -1,10 +1,13 @@ - + + <%= page_title %> <%= render "meta_links", collection: @current_item %> + <%= tag.link rel: "canonical", href: canonical_url %> + <%= tag.link rel: "search", type: "application/opensearchdescription+xml", href: opensearch_url(format: :xml, version: 1), title: "Search posts" %> <%= csrf_meta_tag %> <% unless CurrentUser.enable_desktop_mode? %> @@ -13,48 +16,17 @@ <% if CurrentUser.user.blacklisted_tags.present? %> "> <% end %> - <%= javascript_pack_tag "application" %> <%= stylesheet_pack_tag "application" %> <% if CurrentUser.user.custom_style.present? && params.fetch(:css, "true").truthy? %> <%= stylesheet_link_tag custom_style_users_path(md5: Digest::MD5.hexdigest(CurrentUser.user.custom_style)), media: "screen" %> <% end %> - <% if Danbooru.config.twitter_username.present? %> - - <% end %> - - - <%= tag.meta name: "description", content: meta_description %> + <% if current_page?(root_url) %> + <%= json_ld_website_data %> + <% end %> + + <%= tag.meta name: "description", content: meta_description %> <%= tag.meta property: "og:type", content: "website" %> <%= tag.meta property: "og:site_name", content: Danbooru.config.app_name %> <%= tag.meta property: "og:title", content: page_title %> @@ -69,6 +41,7 @@ <%= tag.meta name: "twitter:description", content: meta_description %> <%= tag.meta name: "git-hash", content: Rails.application.config.x.git_hash %> + <%= tag.meta name: "theme-color", content: "hsl(213, 100%, 50%)" %> <%= yield :html_header %> <%= raw Danbooru.config.custom_html_header_content %> @@ -122,6 +95,8 @@ window.Danbooru.notice = Danbooru.Utility.notice; window.Danbooru.error = Danbooru.Utility.error; + window.$ = Danbooru.jQuery; + window.jQuery = Danbooru.jQuery; <%= render "static/footer" %> diff --git a/app/views/moderation_reports/_new.html.erb b/app/views/moderation_reports/_new.html.erb index 0af8a497c..f433ee805 100644 --- a/app/views/moderation_reports/_new.html.erb +++ b/app/views/moderation_reports/_new.html.erb @@ -1,12 +1,10 @@
<%= embed_wiki("help:report_notice") %> - <%# XXX dtext_field expects there to be a `moderation_report` instance variable. %> - <% @moderation_report = moderation_report %> - <%= edit_form_for(@moderation_report, format: :js, remote: true) do |f| %> + <%= edit_form_for(moderation_report, format: :js, remote: true) do |f| %> <%= f.hidden_field :model_type %> <%= f.hidden_field :model_id %> - <%= dtext_field "moderation_report", "reason", preview_id: "dtext-preview-for-moderation-report", type: "string" %> - <%= dtext_preview_button "moderation_report", "reason", preview_id: "dtext-preview-for-moderation-report" %> + <%= f.input :reason, as: :dtext, inline: true %> + <%= dtext_preview_button "moderation_report_reason" %> <% end %>
diff --git a/app/views/pools/_secondary_links.html.erb b/app/views/pools/_secondary_links.html.erb index 618c9070e..d2b1ca5c9 100644 --- a/app/views/pools/_secondary_links.html.erb +++ b/app/views/pools/_secondary_links.html.erb @@ -5,6 +5,9 @@ <% if policy(Pool).create? %> <%= subnav_link_to "New", new_pool_path %> <% end %> + <% if PoolVersion.enabled? %> + <%= subnav_link_to "Recent changes", pool_versions_path %> + <% end %> <%= subnav_link_to "Help", wiki_page_path("help:pools") %> <% if @pool && !@pool.new_record? %>
  • |
  • @@ -13,10 +16,12 @@ <% if policy(@pool).update? %> <%= subnav_link_to "Edit", edit_pool_path(@pool), "data-shortcut": "e" %> <% end %> - <% if policy(@pool).undelete? %> - <%= subnav_link_to "Undelete", undelete_pool_path(@pool), :method => :post, :remote => true %> - <% elsif policy(@pool).destroy? %> - <%= subnav_link_to "Delete", pool_path(@pool), :method => :delete, :"data-shortcut" => "shift+d", :"data-confirm" => "Are you sure you want to delete this pool?", :remote => true %> + <% if current_page?(action: :edit) %> + <% if policy(@pool).undelete? %> + <%= subnav_link_to "Undelete", undelete_pool_path(@pool), :method => :post, :remote => true %> + <% elsif policy(@pool).destroy? %> + <%= subnav_link_to "Delete", pool_path(@pool), :method => :delete, :"data-shortcut" => "shift+d", :"data-confirm" => "Are you sure you want to delete this pool?", :remote => true %> + <% end %> <% end %> <% if PoolVersion.enabled? %> <%= subnav_link_to "History", pool_versions_path(:search => {:pool_id => @pool.id}) %> diff --git a/app/views/pools/edit.html.erb b/app/views/pools/edit.html.erb index 4202226de..2c2652d6a 100644 --- a/app/views/pools/edit.html.erb +++ b/app/views/pools/edit.html.erb @@ -6,11 +6,11 @@ <%= error_messages_for "pool" %> <%= f.input :name, :as => :string, :input_html => { :value => @pool.pretty_name } %> - <%= dtext_field "pool", "description" %> - <%= dtext_preview_button "pool", "description" %> + <%= f.input :description, as: :dtext %> <%= f.input :post_ids_string, as: :text, label: "Posts" %> <%= f.input :category, :collection => ["series", "collection"], :include_blank => false %> <%= f.button :submit %> + <%= dtext_preview_button "pool_description" %> <% end %> diff --git a/app/views/pools/new.html.erb b/app/views/pools/new.html.erb index 165ec4497..d2f595aee 100644 --- a/app/views/pools/new.html.erb +++ b/app/views/pools/new.html.erb @@ -6,11 +6,11 @@ <%= edit_form_for(@pool) do |f| %> <%= f.input :name, :as => :string, :required => true %> - <%= dtext_field "pool", "description" %> + <%= f.input :description, as: :dtext %> <%= f.input :post_ids_string, as: :text, label: "Posts" %> <%= f.input :category, :collection => ["series", "collection"], :include_blank => true, :selected => "", :required => true %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "pool", "description" %> + <%= dtext_preview_button "pool_description" %> <% end %> diff --git a/app/views/post_appeals/_new.html.erb b/app/views/post_appeals/_new.html.erb index c418a1b8e..8cee19acc 100644 --- a/app/views/post_appeals/_new.html.erb +++ b/app/views/post_appeals/_new.html.erb @@ -1,11 +1,9 @@
    <%= embed_wiki("help:appeal_notice") %> - <%# XXX dtext_field expects there to be a `post_appeal` instance variable. %> - <% @post_appeal = post_appeal %> - <%= edit_form_for(@post_appeal, format: :js, remote: true) do |f| %> + <%= edit_form_for(post_appeal, format: :js, remote: true) do |f| %> <%= f.hidden_field :post_id %> - <%= dtext_field "post_appeal", "reason", preview_id: "dtext-preview-for-post-appeal", type: "string" %> - <%= dtext_preview_button "post_appeal", "reason", preview_id: "dtext-preview-for-post-appeal" %> + <%= f.input :reason, as: :dtext, inline: true %> + <%= dtext_preview_button "post_appeal_reason" %> <% end %>
    diff --git a/app/views/post_flags/_new.html.erb b/app/views/post_flags/_new.html.erb index 00cc67449..1e2005ac3 100644 --- a/app/views/post_flags/_new.html.erb +++ b/app/views/post_flags/_new.html.erb @@ -1,11 +1,9 @@
    <%= embed_wiki("help:flag_notice") %> - <%# XXX dtext_field expects there to be a `post_flag` instance variable. %> - <% @post_flag = post_flag %> - <%= edit_form_for(@post_flag, format: :js, remote: true) do |f| %> + <%= edit_form_for(post_flag, format: :js, remote: true) do |f| %> <%= f.hidden_field :post_id %> - <%= dtext_field "post_flag", "reason", preview_id: "dtext-preview-for-post-flag", type: "string" %> - <%= dtext_preview_button "post_flag", "reason", preview_id: "dtext-preview-for-post-flag" %> + <%= f.input :reason, as: :dtext, inline: true %> + <%= dtext_preview_button "post_flag_reason" %> <% end %>
    diff --git a/app/views/posts/index.sitemap.erb b/app/views/posts/index.sitemap.erb new file mode 100644 index 000000000..260be89b1 --- /dev/null +++ b/app/views/posts/index.sitemap.erb @@ -0,0 +1,30 @@ + + +<%# https://support.google.com/webmasters/answer/178636 %> +<%# https://support.google.com/webmasters/answer/80471 %> + + <% @posts.each do |post| %> + + <%= post_url(post) %> + <%= post.updated_at.iso8601 %> + <% if post.visible? %> + <% if post.is_image? %> + + <%= post.file_url %> + + <% elsif post.is_video? %> + + <%= post.preview_file_url %> + <%= post.file_url %> + <%= post.created_at.iso8601 %> + <%= "Post ##{post.id}" %> + <%= post.tag_string %> + <%= post.rating == "s" ? "yes" : "no" %> + + <% end %> + <% end %> + + <% end %> + diff --git a/app/views/posts/partials/index/_excerpt.html.erb b/app/views/posts/partials/index/_excerpt.html.erb index bda961e4b..cbd048b85 100644 --- a/app/views/posts/partials/index/_excerpt.html.erb +++ b/app/views/posts/partials/index/_excerpt.html.erb @@ -16,8 +16,10 @@ <%= render "tag_relationships/alias_and_implication_list", tag: artist.tag %> <% end %> @@ -32,7 +34,7 @@ <%= render "tag_relationships/alias_and_implication_list", tag: wiki_page.tag %> <% end %> @@ -61,7 +63,7 @@ Creator: <%= link_to_user post_set.favgroup.creator %> <% elsif post_set.has_blank_wiki? %> -

    There is currently no wiki page for the tag <%= link_to_wiki post_set.tag.pretty_name %>. You can <%= link_to "create one", new_wiki_page_path(wiki_page: { title: post_set.tag.name }) %>.

    +

    There is currently no wiki page for the tag <%= link_to_wiki post_set.tag.pretty_name %>. You can <%= link_to "create one", new_wiki_page_path(wiki_page: { title: post_set.tag.name }), rel: "nofollow" %>.

    <%= render "tag_relationships/alias_and_implication_list", tag: post_set.tag %> <% end %> diff --git a/app/views/posts/partials/index/_related.html.erb b/app/views/posts/partials/index/_related.html.erb index d23a094fc..cd0923567 100644 --- a/app/views/posts/partials/index/_related.html.erb +++ b/app/views/posts/partials/index/_related.html.erb @@ -5,19 +5,15 @@ - <% if PopularSearchService.enabled? %> -
  • <%= link_to "Searches", searches_explore_posts_path %>
  • - <% end %> - <% if PostViewCountService.enabled? %> -
  • <%= link_to "Viewed", viewed_explore_posts_path %>
  • - <% end %> +
  • <%= link_to "Searches", searches_explore_posts_path %>
  • +
  • <%= link_to "Viewed", viewed_explore_posts_path %>
  • <% end %> -
  • <%= link_to "Deleted", posts_path(tags: "#{params[:tags]} status:deleted") %>
  • -
  • <%= link_to "Random", random_posts_path(tags: params[:tags]), id: "random-post", "data-shortcut": "r" %>
  • +
  • <%= link_to "Deleted", posts_path(tags: "#{params[:tags]} status:deleted"), rel: "nofollow" %>
  • +
  • <%= link_to "Random", random_posts_path(tags: params[:tags]), id: "random-post", "data-shortcut": "r", rel: "nofollow" %>
  • <% if post_set.query.is_simple_tag? %> -
  • <%= link_to "History", post_versions_path(search: { changed_tags: params[:tags] }) %>
  • +
  • <%= link_to "History", post_versions_path(search: { changed_tags: params[:tags] }), rel: "nofollow" %>
  • <% end %> -
  • <%= link_to "Count", posts_counts_path(:tags => params[:tags]) %>
  • +
  • <%= link_to "Count", posts_counts_path(tags: params[:tags]), rel: "nofollow" %>
  • diff --git a/app/views/posts/partials/index/_seo_meta_tags.html.erb b/app/views/posts/partials/index/_seo_meta_tags.html.erb index 8fbfe0a8c..cedb48987 100644 --- a/app/views/posts/partials/index/_seo_meta_tags.html.erb +++ b/app/views/posts/partials/index/_seo_meta_tags.html.erb @@ -1,6 +1,6 @@ <% if @post_set.query.is_empty_search? %> <% page_title("#{Danbooru.config.app_name}: Anime Image Board", suffix: nil) %> - <% meta_description("#{Danbooru.config.canonical_app_name} is the original anime image 'booru. Find over 3.75 million anime pictures categorized by over 100 million tags.") %> + <% meta_description site_description %> <% atom_feed_tag "Posts", posts_url(format: :atom) %> <% else %> @@ -10,6 +10,10 @@ <% atom_feed_tag "Posts: #{@post_set.tag_string}", posts_url(tags: @post_set.tag_string, format: :atom) %> <% end %> +<% if params[:tags].blank? && @post_set.current_page == 1 %> + <% canonical_url root_url(host: Danbooru.config.hostname) %> +<% end %> + <% if @post_set.hide_from_crawler? %> <% end %> @@ -18,8 +22,6 @@ <% end %> -<%= tag.meta name: "canonical", content: posts_url(tags: params[:tags], host: Danbooru.config.hostname, protocol: "https") %> - <% if @post_set.best_post.present? %> <%= tag.meta property: "og:image", content: @post_set.best_post.open_graph_image_url %> <%= tag.meta name: "twitter:image", content: @post_set.best_post.open_graph_image_url %> diff --git a/app/views/posts/partials/show/_child_notice.html.erb b/app/views/posts/partials/show/_child_notice.html.erb index c363b9c4c..4017e7012 100644 --- a/app/views/posts/partials/show/_child_notice.html.erb +++ b/app/views/posts/partials/show/_child_notice.html.erb @@ -1,4 +1,4 @@ -This post has <%= link_to pluralize(children.length, "child"), posts_path(tags: "parent:#{parent.id}") %> +This post has <%= link_to pluralize(children.length, "child"), posts_path(tags: "parent:#{parent.id}"), rel: "nofollow" %> (<%= link_to_wiki "learn more", "help:post_relationships" %>) <%= link_to("« hide", "#", id: "has-children-relationship-preview-link") %> diff --git a/app/views/posts/partials/show/_edit.html.erb b/app/views/posts/partials/show/_edit.html.erb index 295c1f22d..93a07e9d6 100644 --- a/app/views/posts/partials/show/_edit.html.erb +++ b/app/views/posts/partials/show/_edit.html.erb @@ -7,9 +7,9 @@ <%= render "sources/info" %> <%= edit_form_for(post, html: { id: "form" }) do |f| %> - <%= hidden_field_tag :tags_query, params[:q] %> - <%= hidden_field_tag :pool_id, params[:pool_id] %> - <%= hidden_field_tag :favgroup_id, params[:favgroup_id] %> + <%= f.input :tags_query, as: :hidden, input_html: { id: nil, name: "tags_query", value: params[:q] } %> + <%= f.input :pool_id, as: :hidden, input_html: { id: nil, name: "pool_id", value: params[:pool_id] } %> + <%= f.input :favgroup_id, as: :hidden, input_html: { id: nil, name: "favgroup_id", value: params[:favgroup_id] } %> <%= f.input :old_tag_string, as: :hidden, input_html: { value: post.tag_string } %> <%= f.input :old_parent_id, as: :hidden, input_html: { value: post.parent_id } %> <%= f.input :old_source, as: :hidden, input_html: { value: post.source } %> diff --git a/app/views/posts/partials/show/_image.html.erb b/app/views/posts/partials/show/_image.html.erb index cee1ee17f..2eb887576 100644 --- a/app/views/posts/partials/show/_image.html.erb +++ b/app/views/posts/partials/show/_image.html.erb @@ -1,6 +1,6 @@ <% if policy(post).visible? %> <%= content_tag(:picture) do -%> <%= tag.source media: "(max-width: 660px)", srcset: post.tagged_large_file_url -%> - <%= tag.img width: post.image_width_for(CurrentUser.user), height: post.image_height_for(CurrentUser.user), id: "image", class: "fit-width", "data-original-width": post.image_width, "data-original-height": post.image_height, "data-large-width": post.large_image_width, "data-large-height": post.large_image_height, "data-tags": post.tag_string, alt: post.presenter.humanized_essential_tag_string, "data-uploader": post.uploader.name, "data-rating": post.rating, "data-flags": post.status_flags, "data-parent-id": post.parent_id, "data-has-children": post.has_children?, "data-has-active-children": post.has_active_children?, "data-score": post.score, "data-fav-count": post.fav_count, itemprop: "contentUrl", src: post.file_url_for(CurrentUser.user) %> + <%= tag.img width: post.image_width_for(CurrentUser.user), height: post.image_height_for(CurrentUser.user), id: "image", class: "fit-width", "data-original-width": post.image_width, "data-original-height": post.image_height, "data-large-width": post.large_image_width, "data-large-height": post.large_image_height, "data-tags": post.tag_string, alt: post.presenter.humanized_essential_tag_string, "data-uploader": post.uploader.name, "data-rating": post.rating, "data-flags": post.status_flags, "data-parent-id": post.parent_id, "data-has-children": post.has_children?, "data-has-active-children": post.has_active_children?, "data-score": post.score, "data-fav-count": post.fav_count, src: post.file_url_for(CurrentUser.user) %> <% end -%> <% end %> diff --git a/app/views/posts/partials/show/_information.html.erb b/app/views/posts/partials/show/_information.html.erb index d96af09c4..4e6dae192 100644 --- a/app/views/posts/partials/show/_information.html.erb +++ b/app/views/posts/partials/show/_information.html.erb @@ -1,14 +1,11 @@
    • ID: <%= post.id %>
    • - <% if policy(post).can_view_uploader? %> -
    • - Uploader: <%= link_to_user(post.uploader) %> - <%= link_to "»", posts_path(tags: "user:#{post.uploader.name}") %> -
    • - <% end %> +
    • + Uploader: <%= link_to_user(post.uploader) %> + <%= link_to "»", posts_path(tags: "user:#{post.uploader.name}") %> +
    • Date: <%= link_to time_ago_in_words_tagged(post.created_at), posts_path(tags: "date:#{post.created_at.to_date}") %> -
    • <% if post.approver %>
    • @@ -17,9 +14,9 @@
    • <% end %>
    • - Size: <%= link_to_if policy(post).visible?, number_to_human_size(post.file_size), post.tagged_file_url %> + Size: <%= link_to_if policy(post).visible?, "#{number_to_human_size(post.file_size)} .#{post.file_ext}", post.tagged_file_url %> <% if post.has_dimensions? %> - (<%= post.image_width %>x<%= post.image_height %>) + (<%= post.image_width %>x<%= post.image_height %>) <% end %>
    • Source: <%= post_source_tag(post.source, post.normalized_source) %>
    • diff --git a/app/views/posts/partials/show/_parent_notice.html.erb b/app/views/posts/partials/show/_parent_notice.html.erb index 639e83b30..d4388757f 100644 --- a/app/views/posts/partials/show/_parent_notice.html.erb +++ b/app/views/posts/partials/show/_parent_notice.html.erb @@ -1,8 +1,8 @@ -This post belongs to a <%= link_to "parent", posts_path(tags: "parent:#{parent.id}") %> <% "(deleted)" if parent.is_deleted? %> +This post belongs to a <%= link_to "parent", posts_path(tags: "parent:#{parent.id}"), rel: "nofollow" %> <% "(deleted)" if parent.is_deleted? %> <% children.length.tap do |children_count| %> <% if children_count > 1 %> - and has <%= link_to pluralize(children_count - 1, "sibling"), posts_path(tags: "parent:#{parent.id}") %> + and has <%= link_to pluralize(children_count - 1, "sibling"), posts_path(tags: "parent:#{parent.id}"), rel: "nofollow" %> <% end %> <% end %> diff --git a/app/views/posts/show.html+tooltip.erb b/app/views/posts/show.html+tooltip.erb index ecba391a8..6e139cefe 100644 --- a/app/views/posts/show.html+tooltip.erb +++ b/app/views/posts/show.html+tooltip.erb @@ -1,40 +1,39 @@
      - - <% if policy(@post).can_view_uploader? %> - <%= link_to_user @post.uploader %> - <% end %> + <%= link_to_user @post.uploader %> - - <%= @post.fav_count %> - - - - - <%= @post.score %> - - - - <% if @post.last_commented_at.present? %> - - <%= @post.comments.count %> - - - <% end %> - - <%= link_to posts_path(tags: "date:#{@post.created_at.strftime("%Y-%m-%d")}"), class: "post-tooltip-date post-tooltip-info" do %> - <%= time_ago_in_words_tagged(@post.created_at, compact: true) %> ago - <% end %> + + <%= @post.fav_count %> + - - <%= link_to @post.source_domain, @post.normalized_source, class: "post-tooltip-source post-tooltip-info" if @post.source_domain.present? %> - <%= link_to "#{@post.pretty_rating.downcase}", posts_path(tags: "rating:#{@post.rating}"), class: "post-tooltip-rating post-tooltip-info" %> - <%= link_to "#{@post.image_width}x#{@post.image_height}", @post.file_url, class: "post-tooltip-dimensions post-tooltip-info" %> - - <%= link_to "#", class: "post-tooltip-disable", title: "Disable enhanced tooltips" do %> - - <% end %> + + <%= @post.score %> + + + <% if @post.last_commented_at.present? %> + + <%= @post.comments.count %> + + + <% end %> + + <%= link_to posts_path(tags: "date:#{@post.created_at.strftime("%Y-%m-%d")}"), class: "post-tooltip-date post-tooltip-info" do %> + <%= time_ago_in_words_tagged(@post.created_at, compact: true) %> ago + <% end %> + + <% if @post.source_domain.present? %> + <%= link_to @post.source_domain, @post.normalized_source, class: "post-tooltip-source post-tooltip-info" %> + <% else %> + <%= link_to "no source", posts_path(tags: "source:none"), class: "post-tooltip-source post-tooltip-info" %> + <% end %> + + <%= link_to "#{@post.rating.upcase}", posts_path(tags: "rating:#{@post.pretty_rating}"), class: "post-tooltip-rating post-tooltip-info" %> + <%= link_to "#{@post.image_width}x#{@post.image_height}", @post.file_url, class: "post-tooltip-dimensions post-tooltip-info" %> + + <%= link_to "#", class: "post-tooltip-disable", title: "Disable enhanced tooltips" do %> + + <% end %>
      "> diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index a6c77fbdc..1418f7068 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -1,7 +1,8 @@ <% page_title @post.presenter.humanized_essential_tag_string %> <% meta_description "View this #{@post.image_width}x#{@post.image_height} #{number_to_human_size(@post.file_size)} image" %> - +<% canonical_url post_url(@post, host: Danbooru.config.hostname) %> <% atom_feed_tag "Comments for post ##{@post.id}", comments_url(:atom, search: { post_id: @post.id }) %> + <%= render "posts/partials/common/secondary_links" %> <% content_for(:sidebar) do %> @@ -61,25 +62,17 @@ <% end %> @@ -154,9 +147,11 @@ <% if policy(@post).visible? %> <%= tag.meta property: "og:image", content: @post.open_graph_image_url %> - <% end %> - <%= tag.meta name: "canonical", content: post_url(@post, host: Danbooru.config.hostname, protocol: "https") %> + <% if @post.is_video? %> + <%= json_ld_video_data(@post) %> + <% end %> + <% end %> <% if @post.twitter_card_supported? %> diff --git a/app/views/posts/update.js.erb b/app/views/posts/update.js.erb index 1da54c638..606380c4a 100644 --- a/app/views/posts/update.js.erb +++ b/app/views/posts/update.js.erb @@ -1,8 +1,5 @@ <% if @post.valid? %> var $post = $("#post_<%= @post.id %>"); - <% if !CurrentUser.disable_post_tooltips %> - $post.find("img").qtip("destroy", true); - <% end %> var $new_post = $("<%= j PostPresenter.preview(@post, show_deleted: true) %>"); Danbooru.Blacklist.apply_post($new_post.get(0)); $("#post_<%= @post.id %>").replaceWith($new_post); diff --git a/app/views/robots/index.text.erb b/app/views/robots/index.text.erb index c7f1c0983..57244f162 100644 --- a/app/views/robots/index.text.erb +++ b/app/views/robots/index.text.erb @@ -1,15 +1,20 @@ -Sitemap: <%= root_url %>sitemap.xml - User-agent: * Disallow: / +Allow: /$ + +<% if !Rails.env.production? || Danbooru.config.hostname == request.host %> +Disallow: /*.atom +Disallow: /*.json -<% if Rails.env.production? && Danbooru.config.hostname == request.host %> Allow: /artists Allow: /artist_commentaries Allow: /comments Allow: /explore +Allow: /favorite_groups Allow: /forum_posts Allow: /forum_topics +Allow: /iqdb_queries +Allow: /login Allow: /notes Allow: /pools Allow: /posts @@ -17,7 +22,40 @@ Allow: /sessions Allow: /static Allow: /tags Allow: /uploads -Allow: /user_upgrades +Allow: /user_upgrade Allow: /users Allow: /wiki_pages + +<%# Legacy redirects %> +Allow: /artist +Allow: /comment +Allow: /forum +Allow: /note +Allow: /pool +Allow: /post +Allow: /tag +Allow: /user +Allow: /wiki + +<%# Images %> +Allow: /crop +Allow: /preview +Allow: /original +Allow: /sample +Allow: /data +Allow: /images +Allow: /packs + +<%# Static %> +Allow: /terms_of_service +Allow: /privacy +Allow: /sitemap.xml + +Sitemap: <%= sitemap_url(format: :xml, sitemap: "artists") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "forum_topics") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "pools") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "posts") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "tags") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "users") %> +Sitemap: <%= sitemap_url(format: :xml, sitemap: "wiki_pages") %> <% end %> diff --git a/app/views/static/opensearch.xml.erb b/app/views/static/opensearch.xml.erb new file mode 100644 index 000000000..8652a2f9c --- /dev/null +++ b/app/views/static/opensearch.xml.erb @@ -0,0 +1,8 @@ + + + <%= Danbooru.config.app_name %> + <%= Danbooru.config.app_name %> search + <%= root_url %>favicon.ico + + + diff --git a/app/views/static/privacy_policy.html.erb b/app/views/static/privacy_policy.html.erb new file mode 100644 index 000000000..798efa89f --- /dev/null +++ b/app/views/static/privacy_policy.html.erb @@ -0,0 +1,479 @@ +<% page_title "Privacy Policy" %> + +<% @app_name = Danbooru.config.canonical_app_name %> +<% @canonical_url = Danbooru.config.canonical_url %> +<% @contact_email = Danbooru.config.contact_email %> + +
      +
      +

      Privacy Policy

      + +

      Last updated: July 5, 2020

      + +

      This privacy policy describes what information <%= @app_name %> + (collectively "we", "us", "our") collects about you when you use our website + or services (referred to as our "Site" or "Services"), how we use your + information, and what rights you have in relation to your information.

      + +

      If there are any terms or conditions in this privacy policy that you do + not agree with, please discontinue your use of our Site and our Services. + If you have any questions or concerns about this policy, or our practices + in regards to your personal information, you may contact us at + <%= link_to @contact_email, "mailto:#{@contact_email}" %>.

      + +

      What We Collect

      + +
      Information You Provide To Us
      + +

      Summary: We collect information you directly give us.

      + +

      We collect information you directly provide to us when you use our + Services. What information we collect depends on how you use our Services. + The information we may collect includes:

      + +
        +
      • Account information. + We collect your username, password, and optionally your email address + when you create an account with us. We also store your account + preferences and settings. You may update your account information at + any time, including to change your username or to deactivate your + account, as described below. +
      • + +
      • Content you submit. + We collect content you submit to the Site. This includes images, + videos, or written content you post or upload to the Site, including + posts, comments, forum posts, translation notes, wiki pages, pools, + artist commentaries, and any other contributions you make to the Site. +
      • + +
      • Actions you take. + We collect information about actions you take when using the Site. This + includes favoriting or voting on content, flagging or reporting + content, sending private messages, saving searches, sending private + messages, and viewing forum threads. This also includes any edits or + changes you make to any content on the Site. +
      • + +
      • Payment information. + We collect information necessary to process your payment if you make a + purchase with us. This information includes your credit card number, + security code, and expiration date, your email address, your location + (such as your country, your postal code, your billing address, and your + location at time of purchase), and your IP address. All payment + information is securely stored by Stripe. You may find their privacy + policy at <%= external_link_to "https://stripe.com/privacy" %>. +
      • + +
      • + Other information. + You may choose to provide other information directly to us. For + example, we may collect information when you participate in surveys, + polls, promotions, giveaways, or other site-sponsored activities, or + you request support from us, or you contact us directly by email. +
      • +
      + +
      Information We Collect Automatically
      + +

      Summary: We collect some information about you + automatically, including your IP address, information about your computer, + and information about your site usage.

      + +
        +
      • Log and usage data. We may log information about you + when you visit or use our Services. This information may include your + IP address, browser type and version, computer or device type, device + characteristics such as screen resolution or operating system version, + language and timezone preferences, or your geolocation or country + information. This may also include information about your usage of the + Site, including posts you have viewed, pages you have visited, links + you have clicked, and searches you have performed. +
      • + +
      • Cookies. We may receive information from cookies, + which are pieces of data your browser stores and sends back to us when + making requests, and similar technologies. We use this information to + maintain login sessions and to store account settings and preferences. +
      • +
      + +
      Information We Collect From Other Sources
      + +

      Summary: If you choose to link other sites to your + <%= @app_name %> account, we will receive profile information about you + from those sites.

      + +

      We may receive information about you from other sources and combine that + information with other information we have about you. For example, we may + give you the option to link your <%= @app_name %> account to your accounts + on other sites, such as your Google, Twitter, or other accounts. If you + choose to link a third-party account to your <%= @app_name %> account, we + will collect information about you from that account as described in the + section titled How We Handle Linked Accounts below.

      + +

      How We Use Your Information

      + +

      Summary: We use your information to provide services to + you, to operate the Site, and to enforce our Terms of Service.

      + +

      We use your information to:

      + +
        +
      • + Provide services to you. + We may use your information to provide you with our Services, and to + provide you with support or customer service. +
      • + +
      • + Manage your account. + We may use your information to allow you to login and manage your + account, to store your account preferences, to help you recover access + to your account, and to send you administrative messages or notices + pertaining to you. +
      • + +
      • + Protect our Services. + We may use your information to maintain the safe and secure operation + of our Services, including to enforce our Terms of Service, to detect + and prevent abuse of our Services, to moderate our Services, to block + spam, to prevent fraud, and to debug and troubleshoot problems with the + Site. +
      • + +
      • + Monitor usage of our Services. + We may use your information to maintain and improve our Services, + to analyze trends, to monitor site usage and performance, to generate + internal reports and analytics, to research and develop new Services, + and to provide public reports about usage of the Site, including + information about popular searches, popular pages, and post view + counts. +
      • + +
      • + Facilitate account creation and login. + If you choose to link a third-party account (such as your Google or + Twitter account) to your <%= @app_name %> account, we may use the + information you allowed us to collect from those third parties to + create a new <%= @app_name %> account for you, or to allow you to login + to <%= @app_name %> using your third-party account. See the section + below titled How We Handle Linked Accounts for + further information. +
      • + +
      • + Respond to legal requests. + If we receive a subpoena or other legal request, we may need to inspect + the data we hold to determine how to respond. +
      • +
      + +

      How Information About You Is Shared

      + +

      Summary: Most activity on <%= @app_name %> is public. We + won't share your information with advertisers without your consent. We + won't share information about you with third parties, except as required + under certain conditions.

      + +

      When you use the Site, certain information about you may be shared with + other users, with Site moderators or other authorized users, or with the + general public. For example:

      + +
        +
      • + Any content you create or submit to the Site will be visible to other + users and to the general public, unless stated otherwise. This includes + your uploads, comments, forum posts, pools, wiki pages, translation + notes, and any other contributions you make to the Site. +
      • + +
      • + Any edits you make to the Site will be visible to other users and to + the general public, unless stated otherwise. This includes your tag + edits, pool edits, wiki edits, translation note edits, artist edits, + commentary edits, and any other kind of edit, change, or modification + you make to any other content on the Site. +
      • + +
      • + Your other personal activities on the Site, such as your favorites, + favorite groups, and saved searches, will normally be visible to other + users and to the general public, unless stated otherwise, or you choose + to make these things private. +
      • + +
      • + Certain information, such as your favorites or favorite groups, you may + choose to make private. This information may still be visible to Site + moderators or to other authorized users in connection with legitimate + needs in operating the Site. +
      • + +
      • + Your username will be publicly associated with any content you create + or edit on the Site. The date and time you created or edited the + content will also be publicly visible. Moderators may also be able to + see the IP address associated with any content you create or edit on + the Site. +
      • + +
      • + When other users view your profile, they will be able to see + information about your activities on the Site, such as your username, + signup date, uploads, favorites, votes, post edit history, comments, + forum posts, Gold or Platinum status. +
      • + +
      • + When you send another user a private message, the recipient of that + message will be able to see the content of your message, your username, + and the date and time the message was sent. Recipients may choose to + share your message publicly or with other users. Recipients may + choose to have private messages forwarded to their email accounts and, + as a result, any messages received by those users will be subject to + the terms and policies of the user’s email provider. +
      • + +
      • + We may, at our sole discretion, allow Site moderators or other + authorized users to view private messages for legitimate needs in + connection with operating the Site, such as to prevent spam or to + enforce our Terms of Service. +
      • + +
      • + We may allow third parties to access, collect, archive, or redistribute + information collected from our API or from public data dumps we may + provide. This information may be stored by third parties even after it + has been removed from <%= @app_name %>. +
      • +
      + +

      Otherwise, we do not share, sell, or give away your personal information + to third parties unless one of the following circumstances applies:

      + +
        +
      • + With your consent. + We may share your information with your consent or at your direction. +
      • + +
      • + With our service providers. + We may share information with vendors, consultants, and other service + providers (but not with advertisers or ad partners) who need access to + such information to carry out work for us. The partner's use of + personal data will be subject to appropriate confidentiality and + security measures. +
      • + +
      • + Aggregated or anonymized information. + We may share information about you that has been aggregated or anonymized + such that it cannot reasonably be used to identify you. For example, we + may show the total number of times a post has been visited without + identifying who the visitors were, or how many times a tag has been + searched without identifying who the searchers were. +
      • + +
      • + To enforce our policies and rights. + We may share information if we believe your actions are in violation of + our user agreements, rules, or other policies, or to protect the + rights, property, and safety of ourselves and others. +
      • + +
      • + To comply with the law. + We may share information in response to a request for information if we + believe disclosure is in accordance with, or required by, any applicable + law, regulation, legal process or governmental request, including, but not + limited to, meeting national security or law enforcement requirements. To + the extent the law allows it, we will attempt to provide you with prior + notice before disclosing your information in response to such a request. +
      • + +
      • + In an emergency. + We may share information if we believe it's necessary to prevent imminent + and serious bodily harm to a person. +
      • + +
      • + Business transfers. We may share or transfer your + information in connection with, or during negotiations of, any merger, + sale of company assets, financing, or acquisition of all or a portion + of our business to another company. +
      • +
      + +
      How We Handle Linked Accounts
      + +

      Summary: If you link other sites to your <%= @app_name %> + account, we will receive profile information about you from those sites. We + won't disclose any information about you to those sites.

      + +

      We may offer you the ability to sign up or login using third-party + social media accounts (such as your Google or Twitter accounts), or to link + other third-party accounts to your <%= @app_name %> account. If you choose + to do this, we will receive certain profile information about you from the + third-party service. The profile information we receive may vary depending + on the service concerned, but will often include your username, email + address, profile picture, as well as other information included in your + public profile. + +

      We will use the information we receive only for the purposes that are + described in this Privacy Policy, or to provide features that are described + to you when you link your account. You may unlink third-party accounts + from your account at any time, at which point we will delete any + information about you we have collected from these services.

      + +

      We will not use any information collected about you from third-party + services for marketing purposes without your consent. We will not disclose + any information about you or your usage of our Site to any third-party + websites you have linked your <%= @app_name %> account with. Please note, + however, that we do not control, and are not responsible for, the behavior + of third-party websites. We recommend that you review their privacy + policies to understand how they collect, use and share your personal + information, and how you can set your privacy preferences on their sites + and apps.

      + +

      Controlling Your Information

      + +
      Accessing and Changing Your Information
      + +

      If you would like to review or change the information in your account, + such as your user name, password, email address, or other settings, you + can visit your <%= link_to "account settings", settings_path %> and update + your settings.

      + +
      Deleting Your Account
      + +

      Summary: You may deactivate your account, but we may + retain your IP address and your public contributions to the Site.

      + +

      If you would like to delete your account, you may request your + account to be deactivated <%= link_to "here", maintenance_user_deletion_path %>.

      + +

      When your account is deactivated, we will delete your non-public personal + information from our active systems, including your email address, + password, account preferences, saved searches, and private favorites, and + we will disassociate your username from content you have submitted to + the Site, including your uploads, votes, comments, forum posts, private + messages, translation notes, wiki pages, tag edits, and other public + contributions you have made to the Site.

      + +

      Please note that content that you have submitted to the Site will not + be removed from the Site after deactivating your account. In particular, + your uploads will not be removed from the Site, private messages you have + sent to other users will not be deleted, your comments and forum posts will + still be visible to others unless you delete them before deleting your + account. Comments, forum posts, or messages posted by other users + mentioning you by name or quoting you will still be visible to others.

      + +

      We may retain certain other information about you even after you + deactivate your account, including your IP address, your user ID, your past + usernames, and other identifiers associated with your account. We may + retain this information as required by law or as necessary for legitimate + business needs, such as for preventing spam, abuse, or violations of our + Terms of Service.

      + +
      Controlling Email Communications
      + +

      If you would like to opt out of receiving emails from the Service, you + may unsubscribe by clicking on the unsubscribe link in the emails that we + send or by disabling email notifications in your + <%= link_to "account settings", settings_path %>. We may still send you + emails relating to the use or security of your account, such as when + resetting your password.

      + +
      Controlling Cookies
      + +

      Most web browsers are set to accept cookies by default. If you prefer, + you can usually choose to set your browser to remove or reject first- and + third-party cookies. Please note that if you choose to remove or reject + cookies, this could affect the availability and functionality of our + Site.

      + +

      Other Information

      + +
      Minors
      + +

      Persons under the age of 18 are not allowed to create an account or + otherwise use our Services. By using our Services, you represent that you + are over the age required by the laws of your country to create an account + or otherwise use our Services.

      + +
      Data Retention
      + +

      Summary: We keep your information for as long as + necessary to fulfill the purposes outlined in this privacy policy, unless + otherwise required by law.

      + +

      We store the information we collect for as long as it is necessary for + the purpose(s) for which we originally collected it. We may retain certain + information for legitimate business purposes or as required by law.

      + +

      When we have no ongoing legitimate business need to process your + personal information, we will either delete or anonymize it, or, if this is + not possible (for example, because your personal information has been + stored in backup archives), then we will securely store your personal + information and isolate it from any further processing until deletion is + possible.

      + +
      International Data Transfers
      + +

      Summary: We store data in the United States and operate under US law.

      + +

      We are based in the United States and we process and store information + on servers located in the United States. We may store information on + servers and equipment in other countries depending on a variety of factors, + including the locations of our users and service providers. By accessing or + using the Site or otherwise providing information to us, you consent to + the processing, transfer and storage of information in and to the U.S. and + other countries, where you may not have the same rights as you do under + local law.

      + +
      Information Security
      + +

      Summary: We take reasonable measures to secure your + data. We will promptly notify you of breaches of your personal + information.

      + +

      We take reasonable measures to protect your personal information from + loss, theft, misuse, unauthorized access, disclosure, alteration, or + destruction. We also enforce technical and administrative access controls + to limit access to non-public personal information by staff members or + designated representatives of <%= @app_name %>. However, although we will + do our best to protect your personal information, transmission of personal + information to and from our Services is at your own risk. You should only + access our Services within a secure environment.

      + +

      In the event that we become aware of a privacy breach which has resulted + or may result in unauthorized access, use or disclosure of your personal + information, we will investigate the matter and promptly notify you and the + applicable Supervisory Authority of the breach, unless the breach is + unlikely to result in a risk to the rights and freedoms of natural + persons.

      + +
      Changes to this Policy
      + +

      Summary: We may update this policy from time to time.

      + +

      We may update this Privacy Policy from time to time. The updated policy + will be indicated by a new "Last updated" date at the top of this page. If + we make material changes to this Privacy Policy, we may, at our discretion, + notify you either by prominently posting a notice of such changes or by + directly sending you a notification. We encourage you to review this + Privacy Policy frequently to stay informed of how we handle your personal + information.

      + +

      Changes to this Privacy Policy will go into effect as soon as the + updated Privacy Policy is accessible. By continuing to use our Services + after changes to our Privacy Policy go into effect, you agree to be bound + by the revised policy.

      +
      +
      diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb index 078b9ec7d..fabd7675e 100644 --- a/app/views/static/site_map.html.erb +++ b/app/views/static/site_map.html.erb @@ -72,10 +72,8 @@
      • Reports

      • <%= link_to("Performance Reports", "https://isshiki.donmai.us/user-reports") %>
      • - <% if Danbooru.config.reportbooru_server %> -
      • <%= link_to("Top Searches", searches_explore_posts_path) %>
      • -
      • <%= link_to("Missed Searches", missed_searches_explore_posts_path) %>
      • - <% end %> +
      • <%= link_to("Top Searches", searches_explore_posts_path) %>
      • +
      • <%= link_to("Missed Searches", missed_searches_explore_posts_path) %>
      @@ -140,6 +138,7 @@
    • <%= link_to("Bans", bans_path) %>
    • <%= link_to("Feedback", user_feedbacks_path) %>
    • <%= link_to("Terms of Service", terms_of_service_path) %>
    • +
    • <%= link_to("Privacy Policy", privacy_policy_path) %>
    • Admin

    • diff --git a/app/views/static/sitemap.xml.erb b/app/views/static/sitemap.xml.erb deleted file mode 100644 index 4c8e1ece5..000000000 --- a/app/views/static/sitemap.xml.erb +++ /dev/null @@ -1,41 +0,0 @@ - - - - - <%= posts_url %> - daily - - - <%= wiki_pages_url %> - daily - - - <%= pools_url %> - daily - - - <% cache("sitemap", :expires_in => 24.hours) do %> - <% @popular_search_service.each_search do |tags, count| %> - - <%= posts_url(tags: tags) %> - <%= Date.today %> - - <% end %> - - <% @posts.each do |post| %> - - <%= post_url(post) %> - - - <%= post.file_url %> - - - <%= post.presenter.humanized_essential_tag_string %> - - - <%= post.created_at.to_date %> - - <% end %> - <% end %> - \ No newline at end of file diff --git a/app/views/static/sitemap_index.xml.erb b/app/views/static/sitemap_index.xml.erb new file mode 100644 index 000000000..eaae89833 --- /dev/null +++ b/app/views/static/sitemap_index.xml.erb @@ -0,0 +1,21 @@ + + + + <% 0.upto(@relation.maximum(:id) / @limit) do |page| %> + <% lo = page * @limit %> + <% hi = (page + 1) * @limit %> + <% lastmod = @relation.where(id: lo..hi).maximum(:updated_at)&.iso8601 %> + <% if @sitemap == "posts" %> + <% loc = posts_url(limit: @limit, format: :sitemap, tags: "id:#{lo}..#{hi}") %> + <% else %> + <% loc = polymorphic_url(@relation.klass, limit: @limit, format: :sitemap, search: { id: "#{lo}..#{hi}", **@search }) %> + <% end %> + + <% if lastmod.present? %> + + <%= loc %> + <%= lastmod %> + + <% end %> + <% end %> + diff --git a/app/views/static/terms_of_service.html.erb b/app/views/static/terms_of_service.html.erb index f3eded118..aaaf18ca3 100644 --- a/app/views/static/terms_of_service.html.erb +++ b/app/views/static/terms_of_service.html.erb @@ -1,70 +1,264 @@ <% page_title "Terms of Service" %> +<% @app_name = Danbooru.config.canonical_app_name %> +<% @canonical_url = Danbooru.config.canonical_url %> +<% @contact_email = Danbooru.config.contact_email %> +
      -

      Rules

      - -

      Please read the following before using the site:

      - -
        -
      • Danbooru reserves the right to delete or modify your account, or any content you have posted to the site.
      • -
      • You will make a best faith effort to upload only high quality anime-related images.
      • -
      • You have read the <%= link_to_wiki "tagging guidelines", "help:tags" %>.
      • +

        Terms of Service

        + +

        Last updated: July 5, 2020

        + +

        These Terms of Service ("Terms") govern your access to and use of our + Site and Services. By accessing or using our Site, you agree to be bound by + these Terms. If you do not agree to these Terms, you may not access or use + the Site.

        + +

        In addition to these Terms of Service, the + <%= link_to_wiki "Upload Rules", "help:upload_rules" %>, + <%= link_to_wiki "Community Rules", "help:community_rules" %>, + and <%= link_to "Privacy Policy", privacy_policy_path %> also govern your + use of the Site.

        + +

        Access To The Site

        + +

        Persons under the age of 18 are not allowed to create an account or + otherwise use our Services. By using our Services, you represent that you + are over the age required by the laws of your country to create an + account.

        + +

        We may suspend or terminate your account or your ability to access or + use the Site at any time for any or no reason, including for a violation of + these Terms, the <%= link_to_wiki "Upload Rules", "help:upload_rules" %>, + or the <%= link_to_wiki "Community Rules", "help:community_rules" %>.

        + +

        Things You Cannot Do

        + +

        When accessing or using the Site, you agree to follow these Terms of + Service, the <%= link_to_wiki "Upload Rules", "help:upload_rules" %>, and + the <%= link_to_wiki "Community Rules", "help:community_rules" %>. + +

        When accessing or using the Site, you will not:

        + +
          +
        • + Create multiple accounts to bypass or circumvent bans, account + restrictions, site rules, or to send spam. +
        • + +
        • + Bypass or circumvent any limitations or restrictions placed on your + account, including upload limits, comment limits, rate limits, post + access restrictions, and any other limits. +
        • + +
        • + Share, sell, trade, transfer, or give access to your account to any + other user. +
        • + +
        • + Sell or trade privileged user accounts, including Gold, Platinum, or + Builder accounts. +
        • + +
        • + Send spam or unsolicited messages, including via comments, forum posts, + and private messages. +
        • + +
        • + Attempt to gain unauthorized access to another user's account, the Site, + or to any computer systems or networks used by the Site. +
        • + +
        • + Upload malware, computer viruses, or other malicious content to the + Site, or distribute or link to any such content through the Site. +
        • + +
        • + Prevent or disrupt other users from using the Site, or damage, + overburden, or impair the operation of the Site in any manner. +
        • + +
        • + Create, use, or distribute bots or scripts that cause harm to the Site, + or that allow users to bypass restrictions on the Site. +
        -
        -
        Post Limiting
        -

        You cannot upload a post during the first week of signing up.

        -

        After the initial period, you can post a variable number of posts based on how many of your previous uploads were approved or deleted.

        -
        +

        User-Generated Content

        -
        -
        Prohibited and Restricted Content
        -

        You may not upload any of the following:

        -
          -
        • Non-anime: Photographs of American porn actresses, for example, are prohibited. Photographs of cosplayers, figures, or prominent figures in the industry are acceptable.
        • -
        • Manga: Uploading entire manga or doujinshi chapters is discouraged. Individual pages can be uploaded if they meet the quality criterion.
        • -
        • Fake translations: Images that have false translations are not allowed in any case.
        • -
        - -

        The following may be uploaded, but will be put to a higher level of artistic and qualitative scrutiny than normal, and (if applicable) you must tag them with the corresponding tag. Please do not upload more than 20 a day of these works:

        -
          -
        • Western: Drawings of characters from non-Japanese comics, games, and other properties.
        • -
        • Furry: Any explicit image where a person's skin is made of fur or scales. Please use the furry tag.
        • -
        • Grotesque: Any depiction of extreme mutilation, extreme bodily distension, feces, or bodies that are far outside the realm of normal human proportion (for example, breasts that are larger than three heads in size or penises that are larger than two heads in size). Please use the guro tag.
        • -
        • Self Artwork: You can upload your own artwork but be aware that Danbooru is not a place to expect positive reinforcement. Your uploads will be subject to the same standards as everything else and may be flagged, downvoted, or even deleted. If you don't want to risk experiencing these things then don't upload your art.
        • -
        +

        All content submitted to the Site, including text, links, translations, + images, videos, animations, songs, games, and any other materials + (collectively referred to as "Content") is the sole responsibility of the + person who submitted the Content. We take no responsibility for Content + submitted to the Site by users.

        -

        The following may be uploaded but are generally not recommended unless a better copy cannot be found:

        -
          -
        • Third-Party Watermark: Any image where a person who is not the original copyright owner has placed a watermark on the image.
        • -
        • Hard translations: Images that have been edited to replace the original language are discouraged.
        • -
        • Poorly compressed: Any image where compression artifacts are easily visible. An exception to this is if the image was uploaded by the artist and there are no higher quality versions available.
        • -
        • Nude Filter: Images that have been edited by someone other than the original creator to remove clothing or censorship. These typically feature crude drawings of breasts or genitalia laid on top of the original image.
        • -
        -
        +

        Any Content you submit to the Site must abide by these Terms of Service, the + <%= link_to_wiki "Upload Rules", "help:upload_rules" %>, and the <%= + link_to_wiki "Community Rules", "help:community_rules" %>. Although we have + no obligation to monitor, review, or moderate any Content submitted to the + Site, we may, in our sole discretion, delete or remove access to Content at + any time and for any reason, including for a violation of these Terms, a + violation of our Upload Rules, or if you otherwise create liability for + us.

        -
        -
        Copyright Infringement
        +

        You understand that by using the Site, you may be exposed to Content + that might be considered offensive, harmful, obscene, inaccurate, + mislabeled, mistagged, unlawful in your jurisdiction, or otherwise + inappropriate. The use of any Content posted on the Site or obtained by you + through the Site is at your own risk. We do not endorse, support, represent + or guarantee the completeness, truthfulness, accuracy, or reliability of + any Content or communications posted on the Site or endorse any opinions + expressed via the Site.

        -

        If you believe a post infringes upon your copyright, please send an email to the <%= mail_to Danbooru.config.contact_email, "webmaster", :encode => "hex" %> with the following pieces of information:

        -
          -
        • The URL of the infringing post.
        • -
        • Proof that you own the copyright.
        • -
        -
        +

        By submitting, posting, or uploading any Content to the Site, you + represent and warrant that you have all rights, power, and authority + necessary to grant the rights to Your Content contained within these Terms. + You agree you are responsible for any Content you provide to the Site, + including compliance with any applicable laws, rules, and regulations. + Because you alone are responsible for Your Content, you may expose yourself + to liability if you post or share Content without all necessary rights.

        -
        -
        Privacy Policy
        +

        By submitting Content to the Site, you grant <%= @app_name %> a + worldwide, royalty-free, perpetual, irrevocable, non-exclusive, + transferable, and sublicensable license to use, copy, modify, adapt, + distribute, create derivative works from, allow downloads of, and display + Your Content in all media formats and channels now known or later + developed.

        -

        Danbooru will not disclose the IP address, email address, password, or DMails of any user except to the staff.

        -

        Danbooru is allowed to make public everything else, including but not limited to: uploaded posts, favorited posts, comments, forum posts, wiki edits, and note edits.

        -
        +

        Copyright Infringment

        -
        -
        Accounts
        +

        If you believe any Content on the Site infringes upon your copyright, + you may send an email to the <%= link_to "webmaster", "mailto:#{@contact_email}" %> + with the following pieces of information:

        -

        You may have alternative accounts for bots or scripts, but if there's any suspicion you either create a new account or reuse an existing one to evade a ban or any sort of account limitation (limited uploads, loss of flagging or user feedback permissions), then all your accounts will be banned. Bot accounts that flag posts or leave user feedback are not permitted and will be banned.

        -

        Account sharing is not permitted. If you are suspected of sharing an account, then it will be banned.

        -
        +
          +
        • A list of links to each post you believe infringes on your copyright.
        • +
        • Your contact information.
        • +
        • Proof of your identity.
        • +
        • + A statement affirming that you are the copyright holder for these + works, under penalty of perjury. +
        • +
        + +

        Acceptable proof of identity includes any of the following:

        + +
          +
        • + An email from an email address listed in one of your social media + profiles. +
        • + +
        • + A copy or screenshot of the original .psd file or other source file + of one of your works. +
        • +
        + +

        Please be aware that information about you that does not infringe on + your copyright will not be removed from the Site. This includes artist + tags, artist profiles, wiki pages, comments, forum posts, links to your + social media profiles, and links to your works on other sites.

        + +

        If you knowingly misrepresent that any activity or material on our Site + is infringing, you may be liable for costs and damages.

        + +

        Account Upgrades

        + +

        You may submit your debit card, credit card, or other payment information + ("Payment Information") to purchase a Gold or Platinum account upgrade or + other products or services from the Site. We use third-party service + providers to process your Payment Information, in accordance with our + Privacy Policy. If you submit your Payment Information, you agree to pay + all costs that you incur, and you give us permission to charge you when + payment is due for an amount that includes these costs and any applicable + taxes and fees.

        + +

        We may change the fees and benefits associated with Gold and Platinum + account upgrades from time to time with reasonable advance notice; + provided, however, that no advance notice will be required for temporary + promotions, including temporary reductions in the fees associated with + these account upgrades.

        + +

        API Usage

        + +

        If you use our API, you agree to abide by the rate limits described in + our <%= link_to_wiki "API Documentation", "help:api" %>.

        + +

        Privacy

        + +

        Our <%= link_to "Privacy Policy", privacy_policy_path %> describes how + we handle any information we obtain about you when you use our Services. + You understand that through your use of our Services you consent to the + collection and use of your information as described in our Privacy Policy.

        + +

        Miscellaneous

        + +

        "<%= @app_name %>", "we", or "us" refers to <%= @app_name %>, its + successors, and its assigns. "You" refers to any person who has consented + to these terms or has become contractually bound to them, whether such + person is identified or not at the time.

        + +

        These Terms constitute the entire agreement between you and us regarding + your access to and use of the Services. Our failure to exercise or enforce + any right or provision of these Terms will not operate as a waiver of such + right or provision. If any provision of these Terms is, for any reason, + held to be illegal, invalid or unenforceable, the rest of the Terms will + remain in effect. You may not assign or transfer any of your rights or + obligations under these Terms without our consent. We may freely assign + these Terms.

        + +

        Changes to These Terms

        + +

        We may update these Terms from time to time. The updated Terms will be + indicated by a new "Last updated" date at the top of this page. If we make + material changes to these Terms, we may, at our discretion, notify you + either by prominently posting a notice of such changes or by directly + sending you a notification.

        + +

        Changes to these Terms will go into effect as soon as the updated Terms + are accessible. By continuing to use our Services after changes to these + Terms go into effect, you agree to be bound by the revised Terms.

        + +

        No Warranties

        + +

        THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES + OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, + AND NON-INFRINGEMENT. <%= @app_name.upcase %>, ITS LICENSORS, AND ITS THIRD + PARTY SERVICE PROVIDERS DO NOT WARRANT THAT THE SERVICES ARE ACCURATE, + COMPLETE, RELIABLE, CURRENT, OR ERROR FREE. <%= @app_name.upcase %> DOES + NOT CONTROL, ENDORSE, OR TAKE RESPONSIBILITY FOR ANY CONTENT AVAILABLE ON + OR LINKED TO THE SERVICES OR THE ACTIONS OF ANY THIRD PARTY OR USER, + INCLUDING MODERATORS. WHILE <%= @app_name.upcase %> ATTEMPTS TO MAKE YOUR + ACCESS TO AND USE OF OUR SERVICES SAFE, WE DO NOT REPRESENT OR WARRANT THAT + OUR SERVICES OR SERVERS ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS.

        + +

        Limitation of Liability

        + +

        IN NO EVENT AND UNDER NO THEORY OF LIABILITY, INCLUDING CONTRACT, TORT, + NEGLIGENCE, STRICT LIABILITY, WARRANTY, OR OTHERWISE, WILL THE <%= @app_name.upcase %> + ENTITIES BE LIABLE TO YOU FOR ANY INDIRECT, CONSEQUENTIAL, EXEMPLARY, + INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, OR LOST PROFITS ARISING FROM OR + RELATING TO THESE TERMS OR THE SERVICES, INCLUDING THOSE ARISING FROM OR + RELATING TO CONTENT MADE AVAILABLE ON THE SERVICES THAT IS ALLEGED TO BE + DEFAMATORY, OFFENSIVE, OR ILLEGAL. ACCESS TO, AND USE OF, THE SERVICES IS AT + YOUR OWN DISCRETION AND RISK, AND YOU WILL BE SOLELY RESPONSIBLE FOR ANY + DAMAGE TO YOUR DEVICE OR COMPUTER SYSTEM, OR LOSS OF DATA RESULTING + THEREFROM. IN NO EVENT WILL THE AGGREGATE LIABILITY OF THE <%= @app_name.upcase %> + ENTITIES EXCEED THE GREATER OF ONE HUNDRED U.S. DOLLARS ($100) OR ANY AMOUNT + YOU PAID <%= @app_name.upcase %> IN THE PREVIOUS SIX MONTHS FOR THE SERVICES + GIVING RISE TO THE CLAIM. THE LIMITATIONS OF THIS SECTION WILL APPLY TO ANY + THEORY OF LIABILITY, INCLUDING THOSE BASED ON WARRANTY, CONTRACT, STATUTE, + TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, AND EVEN IF THE <%= @app_name.upcase %> + ENTITIES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY SUCH DAMAGE, AND EVEN IF + ANY REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED ITS ESSENTIAL PURPOSE. + THE FOREGOING LIMITATION OF LIABILITY WILL APPLY TO THE FULLEST EXTENT + PERMITTED BY LAW IN THE APPLICABLE JURISDICTION.

        +
      diff --git a/app/views/tag_aliases/index.html.erb b/app/views/tag_aliases/index.html.erb index 3d8fdfe58..c28772782 100644 --- a/app/views/tag_aliases/index.html.erb +++ b/app/views/tag_aliases/index.html.erb @@ -3,7 +3,7 @@ <%= render "tag_relationships/search", url: tag_aliases_path %> <%= render "listing", :tag_aliases => @tag_aliases %> - <%= numbered_paginator(@tag_aliases) if @tag_aliases.respond_to?(:current_page) %> + <%= numbered_paginator(@tag_aliases) %> diff --git a/app/views/tags/index.sitemap.erb b/app/views/tags/index.sitemap.erb new file mode 100644 index 000000000..c57765581 --- /dev/null +++ b/app/views/tags/index.sitemap.erb @@ -0,0 +1,10 @@ + + + + <% @tags.each do |tag| %> + + <%= posts_url(tags: tag.name) %> + <%= tag.updated_at.iso8601 %> + + <% end %> + diff --git a/app/views/uploads/_image.html.erb b/app/views/uploads/_image.html.erb index 7b0a2208e..4ef2a8d6d 100644 --- a/app/views/uploads/_image.html.erb +++ b/app/views/uploads/_image.html.erb @@ -1,7 +1,7 @@ <% if params[:url] %>

      Size - <% if @remote_size %> + <% if @remote_size.present? %> <%= number_to_human_size(@remote_size) %> <% end %> diff --git a/app/views/uploads/index.html.erb b/app/views/uploads/index.html.erb index 55e75beb2..eb4b63946 100644 --- a/app/views/uploads/index.html.erb +++ b/app/views/uploads/index.html.erb @@ -44,10 +44,12 @@
      <% end %> - - Tags - <%= TagSetPresenter.new(upload.tag_string.split).inline_tag_list_html %> - + <% if policy(upload).can_view_tags? %> + + Tags + <%= TagSetPresenter.new(upload.tag_string.split).inline_tag_list_html %> + + <% end %> <% end %> <% t.column "Uploader" do |upload| %> <%= link_to_user upload.uploader %> diff --git a/app/views/uploads/show.html.erb b/app/views/uploads/show.html.erb index 38925b434..e5392dbc0 100644 --- a/app/views/uploads/show.html.erb +++ b/app/views/uploads/show.html.erb @@ -5,7 +5,9 @@

      • Date: <%= @upload.created_at %>
      • Source: <%= @upload.source %>
      • -
      • Tags: <%= @upload.tag_string %>
      • + <% if policy(@upload).can_view_tags? %> +
      • Tags: <%= @upload.tag_string %>
      • + <% end %> <% if @upload.md5.present? %>
      • MD5: <%= @upload.md5 %> <% end %> @@ -13,7 +15,7 @@
      • Size: <%= number_to_human_size(@upload.file_size) %>
      • <% if @upload.image_width.present? %> - (<%= @upload.image_width %>x<%= @upload.image_height %>) + (<%= @upload.image_width %>x<%= @upload.image_height %>) <% end %> <% end %> diff --git a/app/views/user_feedbacks/edit.html.erb b/app/views/user_feedbacks/edit.html.erb index d7571c462..9758a573d 100644 --- a/app/views/user_feedbacks/edit.html.erb +++ b/app/views/user_feedbacks/edit.html.erb @@ -9,10 +9,10 @@ <%= edit_form_for(@user_feedback) do |f| %> <%= f.input :category, :collection => ["positive", "neutral", "negative"], :include_blank => false %> - <%= dtext_field "user_feedback", "body" %> + <%= f.input :body, as: :dtext %> <%= f.input :is_deleted, label: "Deleted" %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "user_feedback", "body" %> + <%= dtext_preview_button "user_feedback_body" %> <% end %> diff --git a/app/views/user_feedbacks/new.html.erb b/app/views/user_feedbacks/new.html.erb index 67a3d89d5..debad85a6 100644 --- a/app/views/user_feedbacks/new.html.erb +++ b/app/views/user_feedbacks/new.html.erb @@ -16,9 +16,9 @@ <%= edit_form_for(@user_feedback) do |f| %> <%= f.input :user_name, :label => "User", :input_html => { value: @user_feedback.user.try(:name), data: { autocomplete: "user" } } %> <%= f.input :category, :collection => ["positive", "neutral", "negative"], :include_blank => false %> - <%= dtext_field "user_feedback", "body" %> + <%= f.input :body, as: :dtext %> <%= f.button :submit, "Submit" %> - <%= dtext_preview_button "user_feedback", "body" %> + <%= dtext_preview_button "user_feedback_body" %> <% end %> diff --git a/app/views/users/index.sitemap.erb b/app/views/users/index.sitemap.erb new file mode 100644 index 000000000..9a9134097 --- /dev/null +++ b/app/views/users/index.sitemap.erb @@ -0,0 +1,9 @@ + + + + <% @users.each do |user| %> + + <%= user_url(user) %> + + <% end %> + diff --git a/app/views/users/show.html+tooltip.erb b/app/views/users/show.html+tooltip.erb new file mode 100644 index 000000000..bfba26ab2 --- /dev/null +++ b/app/views/users/show.html+tooltip.erb @@ -0,0 +1,135 @@ +
        +
        + +
        + <%= link_to_user @user %> + + <% if @user.is_banned? %> + <%= link_to "Banned", users_path(search: { is_banned: true }), class: "user-tooltip-badge user-tooltip-badge-banned" %> + <% elsif @user.is_admin? %> + <%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %> + <% elsif @user.is_moderator? %> + <%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %> + <% elsif @user.can_approve_posts? %> + <%= link_to "Approver", users_path(search: { can_approve_posts: true }), class: "user-tooltip-badge user-tooltip-badge-approver" %> + <% elsif @user.can_upload_free? %> + <%= link_to "Contributor", users_path(search: { can_upload_free: true }), class: "user-tooltip-badge user-tooltip-badge-contributor" %> + <% else %> + <%= link_to @user.level_string, users_path(search: { level: @user.level }), class: "user-tooltip-badge user-tooltip-badge-#{@user.level_string.downcase}" %> + <% end %> + + <% if @user.positive_feedback_count > 0 %> + <%= link_to user_feedbacks_path(search: { user_id: @user.id }), class: "link-plain user-tooltip-badge user-tooltip-badge-positive-feedback" do %> + + <%= @user.positive_feedback_count %> + <% end %> + <% elsif @user.negative_feedback_count > 0 %> + <%= link_to user_feedbacks_path(search: { user_id: @user.id }), class: "link-plain user-tooltip-badge user-tooltip-badge-negative-feedback" do %> + + <%= @user.negative_feedback_count %> + <% end %> + <% end %> +
        + +
        + <%= time_tag @user.created_at.to_date.iso8601, @user.created_at, class: "user-tooltip-created-at" %> + <% if @user.last_ip_addr.present? && policy(IpAddress).show? %> + · <%= link_to_ip @user.last_ip_addr, shorten: true, class: "link-plain" %> + <% end %> + + <% @user.user_name_change_requests.visible(CurrentUser.user).count.tap do |name_change_count| %> + <% if name_change_count > 0 %> + · <%= link_to pluralize(name_change_count, "other name"), user_name_change_requests_path(search: { user_id: @user.id }), class: "link-plain" %> + <% end %> + <% end %> +
        + + + + + +
          +
        • + <%= link_to new_dmail_path(dmail: { to_id: @user.id }) do %> + + Send Message + <% end %> +
        • + + <% if !@user.is_platinum? %> +
        • + <%= link_to new_user_upgrade_path(user_id: @user.id) do %> + + Gift Upgrade + <% end %> +
        • + <% end %> + + <% if policy(UserFeedback.new(user: @user)).create? %> +
        • + <%= link_to new_user_feedback_path(user_feedback: { user_id: @user.id }) do %> + + Give Feedback + <% end %> +
        • + <% end %> + + <% if policy(CurrentUser.user).promote? %> +
        • + <%= link_to edit_admin_user_path(@user.id) do %> + + Promote User + <% end %> +
        • + <% end %> + + <% if policy(Ban.new(user: @user)).create? %> +
        • + <%= link_to new_ban_path(ban: { user_id: @user.id }) do %> + + Ban User + <% end %> +
        • + <% end %> +
        +
        + +
          +
        • + <%= link_to posts_path(tags: "user:#{@user.name}"), class: "link-plain" do %> +
          <%= humanized_number(@user.post_upload_count) %>
          +
          Uploads
          + <% end %> +
        • +
        • + <%= link_to post_versions_path(search: { updater_id: @user.id }), class: "link-plain" do %> +
          <%= humanized_number(@user.post_update_count) %>
          +
          Tag Edits
          + <% end %> +
        • +
        • + <%= link_to note_versions_path(search: { updater_id: @user.id }), class: "link-plain" do %> +
          <%= humanized_number(@user.note_update_count) %>
          +
          Note Edits
          + <% end %> +
        • +
        • + <%= link_to posts_path(tags: "ordfav:#{@user.name}"), class: "link-plain" do %> +
          <%= humanized_number(@user.favorite_count) %>
          +
          Favorites
          + <% end %> +
        • +
        • + <%= link_to comments_path(search: { creator_id: @user.id }), class: "link-plain" do %> +
          <%= humanized_number(@user.comment_count) %>
          +
          Comments
          + <% end %> +
        • +
        • + <%= link_to forum_posts_path(search: { creator_id: @user.id }), class: "link-plain" do %> +
          <%= humanized_number(@user.forum_post_count) %>
          +
          Forum Posts
          + <% end %> +
        • +
        +
        diff --git a/app/views/wiki_pages/_form.html.erb b/app/views/wiki_pages/_form.html.erb index bb0100e7d..8f5616dc7 100644 --- a/app/views/wiki_pages/_form.html.erb +++ b/app/views/wiki_pages/_form.html.erb @@ -5,7 +5,7 @@ <%= f.input :title, error: false, input_html: { data: { autocomplete: "tag" } }, hint: "Change to rename this wiki page. Update any wikis linking to this page first." %> <%= f.input :other_names_string, as: :text, input_html: { size: "30x1" }, label: "Other names (#{link_to_wiki "help", "help:translated_tags"})".html_safe, hint: "Names used for this tag on other sites such as Pixiv. Separate with spaces." %> - <%= dtext_field "wiki_page", "body" %> + <%= f.input :body, as: :dtext %> <% if policy(@wiki_page).can_edit_locked? %> <%= f.input :is_locked, label: "Locked", hint: "Locked wikis can only be edited by Builders." %> @@ -14,6 +14,6 @@ <%= f.input :is_deleted, label: "Deleted", hint: "Check to mark this wiki page as deleted." %> <%= f.submit "Submit" %> - <%= dtext_preview_button "wiki_page", "body" %> + <%= dtext_preview_button "wiki_page_body" %> <% end %> diff --git a/app/views/wiki_pages/_secondary_links.html.erb b/app/views/wiki_pages/_secondary_links.html.erb index 276591965..683438372 100644 --- a/app/views/wiki_pages/_secondary_links.html.erb +++ b/app/views/wiki_pages/_secondary_links.html.erb @@ -16,10 +16,12 @@ <% if policy(@wiki_page).edit? %> <%= subnav_link_to "Edit", edit_wiki_page_path(@wiki_page.id), "data-shortcut": "e" %> - <% if @wiki_page.is_deleted? %> - <%= subnav_link_to "Undelete", wiki_page_path(@wiki_page.id), remote: true, method: :put, "data-params": "wiki_page[is_deleted]=false", "data-shortcut": "shift+d", "data-confirm": "Are you sure you want to undelete this wiki?" %> - <% else %> - <%= subnav_link_to "Delete", wiki_page_path(@wiki_page.id), remote: true, method: :put, "data-params": "wiki_page[is_deleted]=true", "data-shortcut": "shift+d", "data-confirm": "Are you sure you want to delete this wiki?" %> + <% if current_page?(action: :edit) %> + <% if @wiki_page.is_deleted? %> + <%= subnav_link_to "Undelete", wiki_page_path(@wiki_page.id), remote: true, method: :put, "data-params": "wiki_page[is_deleted]=false", "data-shortcut": "shift+d", "data-confirm": "Are you sure you want to undelete this wiki?" %> + <% else %> + <%= subnav_link_to "Delete", wiki_page_path(@wiki_page.id), remote: true, method: :put, "data-params": "wiki_page[is_deleted]=true", "data-shortcut": "shift+d", "data-confirm": "Are you sure you want to delete this wiki?" %> + <% end %> <% end %> <% end %> <% elsif @wiki_page_version %> diff --git a/app/views/wiki_pages/new.html.erb b/app/views/wiki_pages/new.html.erb index 1449e530b..aa796d340 100644 --- a/app/views/wiki_pages/new.html.erb +++ b/app/views/wiki_pages/new.html.erb @@ -14,9 +14,9 @@ <%= edit_form_for(@wiki_page) do |f| %> <%= f.input :title, error: false, input_html: { data: { autocomplete: "tag" } } %> <%= f.input :other_names_string, as: :text, input_html: { size: "30x1" }, label: "Other names (#{link_to_wiki "help", "help:translated_tags"})".html_safe, hint: "Names used for this tag on other sites such as Pixiv. Separate with spaces." %> - <%= dtext_field "wiki_page", "body" %> + <%= f.input :body, as: :dtext %> <%= f.submit "Submit" %> - <%= dtext_preview_button "wiki_page", "body" %> + <%= dtext_preview_button "wiki_page_body" %> <% end %> <%= render "tag_relationships/alias_and_implication_list", tag: @wiki_page.tag %> diff --git a/app/views/wiki_pages/search.html.erb b/app/views/wiki_pages/search.html.erb index 4fb24c4fb..3297d9695 100644 --- a/app/views/wiki_pages/search.html.erb +++ b/app/views/wiki_pages/search.html.erb @@ -4,6 +4,8 @@ <%= f.input :title_normalize, label: "Title", hint: "Use * for wildcard searches", input_html: { "data-autocomplete": "wiki-page" } %> <%= f.input :other_names_match, label: "Other names", hint: "Use * for wildcard searches" %> <%= f.input :body_matches, label: "Body" %> + <%= f.input :linked_to, hint: "Which wikis link to the specified wiki.", input_html: { "data-autocomplete": "wiki-page" } %> + <%= f.input :not_linked_to, hint: "Which wikis do not link to the specified wiki.", input_html: { "data-autocomplete": "wiki-page" } %> <%= f.input :other_names_present, as: :select %> <%= f.input :hide_deleted, as: :select, include_blank: false %> <%= f.input :order, collection: [%w[Name title], %w[Date time], %w[Posts post_count]], include_blank: false %> diff --git a/app/views/wiki_pages/show.html.erb b/app/views/wiki_pages/show.html.erb index de2863565..af7285b70 100644 --- a/app/views/wiki_pages/show.html.erb +++ b/app/views/wiki_pages/show.html.erb @@ -23,12 +23,12 @@ <% end %> <% if @wiki_page.new_record? %> -

        This wiki page does not exist. <%= link_to "Create new wiki page", new_wiki_page_path(wiki_page: { title: @wiki_page.title }) %>.

        +

        This wiki page does not exist. <%= link_to "Create new wiki page", new_wiki_page_path(wiki_page: { title: @wiki_page.title }), rel: "nofollow" %>.

        <% else %> <%= format_text(@wiki_page.body) %> <% end %> - <% if @wiki_page.artist %> + <% if @wiki_page.artist.present? && !@wiki_page.artist.is_deleted? %>

        <%= link_to "View artist", @wiki_page.artist %>

        <% end %> diff --git a/config/cable.yml b/config/cable.yml deleted file mode 100644 index feea78f65..000000000 --- a/config/cable.yml +++ /dev/null @@ -1,10 +0,0 @@ -development: - adapter: async - -test: - adapter: test - -production: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: danbooru_production diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index c3f22f050..2f6317b71 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -28,6 +28,11 @@ module Danbooru Socket.gethostname end + # The canonical url for the site (e.g. https://danbooru.donmai.us) + def canonical_url + "https://#{hostname}" + end + # Contact email address of the admin. def contact_email "webmaster@#{hostname}" @@ -317,13 +322,15 @@ module Danbooru # A list of tags that should be removed when a post is replaced. Regexes allowed. def post_replacement_tag_removals %w[replaceme .*_sample resized upscaled downscaled md5_mismatch - jpeg_artifacts corrupted_image source_request non-web_source] + jpeg_artifacts corrupted_image missing_image missing_sample missing_thumbnail + resolution_mismatch source_larger source_smaller source_request non-web_source] end # Posts with these tags will be highlighted in the modqueue. def modqueue_warning_tags %w[hard_translated self_upload nude_filter third-party_edit screencap - duplicate image_sample md5_mismatch resized upscaled downscaled] + duplicate image_sample md5_mismatch resized upscaled downscaled + resolution_mismatch source_larger source_smaller] end def stripe_secret_key @@ -338,22 +345,6 @@ module Danbooru def twitter_api_secret end - # The default headers to be sent with outgoing http requests. Some external - # services will fail if you don't set a valid User-Agent. - def http_headers - { - "User-Agent" => "#{Danbooru.config.canonical_app_name}/#{Rails.application.config.x.git_hash}" - } - end - - def httparty_options - # proxy example: - # {http_proxyaddr: "", http_proxyport: "", http_proxyuser: nil, http_proxypass: nil} - { - headers: Danbooru.config.http_headers - } - end - # you should override this def email_key "zDMSATq0W3hmA5p3rKTgD" @@ -369,19 +360,30 @@ module Danbooru nil end + def twitter_url + return nil unless Danbooru.config.twitter_username.present? + "https://twitter.com/#{Danbooru.config.twitter_username}" + end + # include essential tags in image urls (requires nginx/apache rewrites) def enable_seo_post_urls false end - # reportbooru options - see https://github.com/r888888888/reportbooru + # The URL for the Reportbooru server (https://github.com/evazion/reportbooru). + # Optional. Used for tracking post views, popular searches, and missed searches. + # Set to http://localhost/mock/reportbooru to enable a fake reportbooru + # server for development purposes. def reportbooru_server end def reportbooru_key end - # iqdbs options - see https://github.com/r888888888/iqdbs + # The URL for the IQDBs server (https://github.com/evazion/iqdbs). + # Optional. Used for dupe detection and reverse image searches. + # Set to http://localhost/mock/iqdbs to enable a fake iqdb server for + # development purposes. def iqdbs_server end @@ -459,6 +461,10 @@ module Danbooru def cloudflare_zone end + # The URL for the recommender server (https://github.com/evazion/recommender). + # Optional. Used to generate post recommendations. + # Set to http://localhost/mock/recommender to enable a fake recommender + # server for development purposes. def recommender_server end diff --git a/config/docker/Dockerfile.danbooru b/config/docker/Dockerfile.danbooru index df2af68bd..013644a89 100644 --- a/config/docker/Dockerfile.danbooru +++ b/config/docker/Dockerfile.danbooru @@ -13,14 +13,20 @@ RUN \ webpack \ libvips-dev \ libxml2-dev \ + libxslt-dev \ + zlib1g-dev \ postgresql-server-dev-all && \ # webpacker expects the binary to be called `yarn`, but debian/ubuntu installs it as `yarnpkg`. ln -sf /usr/bin/yarnpkg /usr/bin/yarn WORKDIR /build +COPY .bundle .bundle COPY Gemfile Gemfile.lock ./ -RUN BUNDLE_DEPLOYMENT=true bundle install --jobs 4 +RUN \ + bundle config set deployment true --local && \ + bundle config set path vendor/bundle && \ + bundle install --jobs 4 COPY package.json yarn.lock ./ RUN yarn install @@ -44,6 +50,8 @@ RUN \ mkvtoolnix \ libvips \ libxml2 \ + libxslt1.1 \ + zlib1g \ postgresql-client USER danbooru diff --git a/config/initializers/mechanize_patch.rb b/config/initializers/mechanize_patch.rb deleted file mode 100644 index 875c28b9d..000000000 --- a/config/initializers/mechanize_patch.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'mechanize' - -if Rails.env.test? - # something about the root certs on the travis ci image causes Mechanize - # to intermittently fail. this is a monkey patch to reset the connection - # after every request to avoid dealing wtiht he issue. - # - # from http://scottwb.com/blog/2013/11/09/defeating-the-infamous-mechanize-too-many-connection-resets-bug/ - class Mechanize::HTTP::Agent - MAX_RESET_RETRIES = 10 - - # We need to replace the core Mechanize HTTP method: - # - # Mechanize::HTTP::Agent#fetch - # - # with a wrapper that handles the infamous "too many connection resets" - # Mechanize bug that is described here: - # - # https://github.com/sparklemotion/mechanize/issues/123 - # - # The wrapper shuts down the persistent HTTP connection when it fails with - # this error, and simply tries again. In practice, this only ever needs to - # be retried once, but I am going to let it retry a few times - # (MAX_RESET_RETRIES), just in case. - # - def fetch_with_retry( - uri, - method = :get, - headers = {}, - params = [], - referer = current_page, - redirects = 0 - ) - action = "#{method.to_s.upcase} #{uri}" - retry_count = 0 - - begin - fetch_without_retry(uri, method, headers, params, referer, redirects) - rescue Net::HTTP::Persistent::Error => e - # Pass on any other type of error. - raise unless e.message =~ /too many connection resets/ - - # Pass on the error if we've tried too many times. - if retry_count >= MAX_RESET_RETRIES - print "R" - # puts "**** WARN: Mechanize retried connection reset #{MAX_RESET_RETRIES} times and never succeeded: #{action}" - raise - end - - # Otherwise, shutdown the persistent HTTP connection and try again. - print "R" - # puts "**** WARN: Mechanize retrying connection reset error: #{action}" - retry_count += 1 - self.http.shutdown - retry - end - end - - # Alias so #fetch actually uses our new #fetch_with_retry to wrap the - # old one aliased as #fetch_without_retry. - alias fetch_without_retry fetch - alias fetch fetch_with_retry - end -end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc1899682..35490377a 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,3 +2,4 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf +Mime::Type.register_alias "application/xml", :sitemap diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 65c15ba19..2aaf05ca0 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -1,3 +1,14 @@ +# frozen_string_literal: true +# +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components to know +# more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + +require "dtext_input" + +# # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Wrappers are used by the form builder to generate a @@ -43,6 +54,7 @@ SimpleForm.setup do |config| b.optional :readonly ## Inputs + # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label_input b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } @@ -61,7 +73,13 @@ SimpleForm.setup do |config| # Defaults to :nested for bootstrap config. # inline: input + label # nested: label > input - config.boolean_style = :nested + # + # XXX We use inline instead of nested so that 1) the html structure for + # checkboxes is the same as the html structure of other input elements and 2) + # because if we use the `namespace` option to provide namespaced html IDs on + # a form with nested checkboxes, then SimpleForm generates incorrect IDs on + # the hidden checkbox inputs. + config.boolean_style = :inline # Default class for buttons config.button_class = 'btn' @@ -77,9 +95,6 @@ SimpleForm.setup do |config| # CSS class to add for error notification helper. config.error_notification_class = 'error_notification' - # ID to add for error notification helper. - # config.error_notification_id = nil - # Series of attempts to detect a default label method for collection. # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] @@ -100,7 +115,7 @@ SimpleForm.setup do |config| # config.item_wrapper_class = nil # How the label text should be generated altogether with the required text. - # config.label_text = ->(label, required, explicit_label) { "#{required} #{label}" } + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } # You can define the class to use on all labels. Default is nil. # config.label_class = nil @@ -122,9 +137,6 @@ SimpleForm.setup do |config| # change this configuration to true. config.browser_validations = false - # Collection of methods to detect if a file type was given. - # config.file_methods = [ :mounted_as, :file?, :public_filename ] - # Custom mappings for input types. This should be a hash containing a regexp # to match as key, and the input type that will be used when the field name # matches the regexp as value. @@ -165,4 +177,8 @@ SimpleForm.setup do |config| # Defines which i18n scope will be used in Simple Form. # config.i18n_scope = 'simple_form' + + # Defines validation classes to the input_field. By default it's nil. + # config.input_field_valid_class = 'is-valid' + # config.input_field_error_class = 'is-invalid' end diff --git a/config/routes.rb b/config/routes.rb index 95392b1e1..38b8a87e7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,6 +69,7 @@ Rails.application.routes.draw do get :search end end + resources :autocomplete, only: [:index] resources :bans resources :bulk_update_requests do member do @@ -364,13 +365,24 @@ Rails.application.routes.draw do get "/wiki/recent_changes" => redirect {|params, req| "/wiki_page_versions?search[updater_id]=#{req.params[:user_id]}"} get "/wiki/history/:title" => redirect("/wiki_page_versions?title=%{title}") - get "/sitemap" => "static#sitemap" + get "/sitemap" => "static#sitemap_index" + get "/opensearch" => "static#opensearch", :as => "opensearch" + get "/privacy" => "static#privacy_policy", :as => "privacy_policy" + get "/terms_of_service" => "static#terms_of_service", :as => "terms_of_service" get "/static/keyboard_shortcuts" => "static#keyboard_shortcuts", :as => "keyboard_shortcuts" get "/static/bookmarklet" => "static#bookmarklet", :as => "bookmarklet" get "/static/site_map" => "static#site_map", :as => "site_map" - get "/static/terms_of_service" => "static#terms_of_service", :as => "terms_of_service" get "/static/contact" => "static#contact", :as => "contact" get "/static/dtext_help" => "static#dtext_help", :as => "dtext_help" + get "/static/terms_of_service" => redirect { "/terms_of_service" } + + get "/mock/recommender/recommend/:user_id" => "mock_services#recommender_recommend", as: "mock_recommender_recommend" + get "/mock/recommender/similiar/:post_id" => "mock_services#recommender_similar", as: "mock_recommender_similar" + get "/mock/reportbooru/missed_searches" => "mock_services#reportbooru_missed_searches", as: "mock_reportbooru_missed_searches" + get "/mock/reportbooru/post_searches/rank" => "mock_services#reportbooru_post_searches", as: "mock_reportbooru_post_searches" + get "/mock/reportbooru/post_views/rank" => "mock_services#reportbooru_post_views", as: "mock_reportbooru_post_views" + get "/mock/iqdbs/similar" => "mock_services#iqdbs_similar", as: "mock_iqdbs_similar" + post "/mock/iqdbs/similar" => "mock_services#iqdbs_similar" root :to => "posts#index" diff --git a/config/secrets.yml b/config/secrets.yml deleted file mode 100644 index a3d89cfec..000000000 --- a/config/secrets.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: bcc62a512b9c055c292c17742f1e65bd6d88fa37f4d01c8475103809f3ac4c03e3e98605c47d55cd8801333010ea98920a61b722770629926759624bce732539 - -test: - secret_key_base: 60e32a818af77bdfc40bca866e3b4d7b88d7ba767057ffc9e4532279358af8c67d42f2b99c084b700727303ce25b812a592b52723ebc1e3b812fd09a1f969435 - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> - -staging: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> - \ No newline at end of file diff --git a/config/storage.yml b/config/storage.yml deleted file mode 100644 index d32f76e8f..000000000 --- a/config/storage.yml +++ /dev/null @@ -1,34 +0,0 @@ -test: - service: Disk - root: <%= Rails.root.join("tmp/storage") %> - -local: - service: Disk - root: <%= Rails.root.join("storage") %> - -# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) -# amazon: -# service: S3 -# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> -# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> -# region: us-east-1 -# bucket: your_own_bucket - -# Remember not to checkin your GCS keyfile to a repository -# google: -# service: GCS -# project: your_project -# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket - -# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) -# microsoft: -# service: AzureStorage -# storage_account_name: your_account_name -# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name - -# mirror: -# service: Mirror -# primary: local -# mirrors: [ amazon, google, microsoft ] diff --git a/config/unicorn/production.rb b/config/unicorn/production.rb index e53d8a24f..7ac586706 100644 --- a/config/unicorn/production.rb +++ b/config/unicorn/production.rb @@ -6,7 +6,7 @@ worker_processes 20 timeout 180 # listen "127.0.0.1:9000", :tcp_nopush => true -listen "/tmp/.unicorn.sock", :backlog => 512 +listen "/tmp/.unicorn.sock", backlog: 1024 # Spawn unicorn master worker for user apps (group: apps) user 'danbooru', 'danbooru' diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 466faa138..607382c5e 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -6,8 +6,8 @@ environment.loaders.append('erb', erb); environment.config.output.library = ["Danbooru"]; -environment.config.externals = { - jquery: "jQuery" -} +environment.config.set("resolve.alias", { + "jquery": "jquery/src/jquery.js" +}); module.exports = environment diff --git a/lib/templates/erb/scaffold/_form.html.erb b/lib/templates/erb/scaffold/_form.html.erb index 201a069e2..0d60e8e71 100644 --- a/lib/templates/erb/scaffold/_form.html.erb +++ b/lib/templates/erb/scaffold/_form.html.erb @@ -1,5 +1,6 @@ +<%# frozen_string_literal: true %> <%%= simple_form_for(@<%= singular_table_name %>) do |f| %> - <%%= f.error_notification %> + <%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
        <%- attributes.each do |attribute| -%> diff --git a/package.json b/package.json index 28fa41a81..66940300e 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,14 @@ "@fortawesome/fontawesome-free": "^5.11.2", "@rails/ujs": "^6.0.2-1", "@rails/webpacker": "^5.0.0", - "debug-loader": "^0.0.1", "dropzone": "^5.5.1", - "expose-loader": "^0.7.5", "hammerjs": "^2.0.8", + "jquery": "3.5.1", "jquery-hotkeys": "^0.2.2", "jquery-ui": "^1.12.1", - "qtip2": "^3.0.3", "rails-erb-loader": "^5.5.0", - "script-loader": "^0.7.2", "spark-md5": "^3.0.0", - "stupid-table-plugin": "^1.1.3", + "tippy.js": "^6.2.3", "typeface-anton": "^0.0.72", "typeface-archivo-narrow": "^1.0.0", "typeface-ibm-plex-mono": "^0.0.61", @@ -27,8 +24,10 @@ "webpack-cli": "^3.3.0" }, "devDependencies": { - "eslint": "^6.0.0", + "babel-eslint": "^10.1.0", + "eslint": "^7.0.0", "eslint-loader": "^4.0.0", + "eslint-plugin-babel": "^5.3.0", "eslint-plugin-ignore-erb": "^0.1.1", "stylelint": "^13.0.0", "stylelint-config-standard": "^20.0.0", diff --git a/public/images/danbooru-logo-500x500.png b/public/images/danbooru-logo-500x500.png new file mode 100644 index 000000000..5e652ad82 Binary files /dev/null and b/public/images/danbooru-logo-500x500.png differ diff --git a/script/fixes/065_fix_invalid_wiki_page_titles.rb b/script/fixes/065_fix_invalid_wiki_page_titles.rb new file mode 100755 index 000000000..a33c04e80 --- /dev/null +++ b/script/fixes/065_fix_invalid_wiki_page_titles.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby + +require_relative "../../config/environment" + +CurrentUser.user = User.system +CurrentUser.ip_addr = "127.0.0.1" + +# Fix wiki page titles containing invalid space characters. +WikiPage.transaction do + WikiPage.where("title ~ '\\s'").find_each do |wiki_page| + wiki_page.normalize_title + wiki_page.save! + # handle name conflicts + rescue ActiveRecord::RecordInvalid + wiki_page.normalize_title + wiki_page.title = "#{wiki_page.title}_#{wiki_page.id}" + wiki_page.save! + ensure + puts "wiki id=#{wiki_page.id} title=#{wiki_page.title}" + end +end diff --git a/script/install/app_server.sh b/script/install/app_server.sh deleted file mode 100644 index a5a14d8ac..000000000 --- a/script/install/app_server.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -# this is a version of the install script designed to be run on -# app servers (that is, they won't install PostgreSQL server). -# -# Run: curl -L -s https://raw.githubusercontent.com/r888888888/danbooru/master/script/install/app_server.sh | sh - -export RUBY_VERSION=2.6.3 -export GITHUB_INSTALL_SCRIPTS=https://raw.githubusercontent.com/r888888888/danbooru/master/script/install -export VIPS_VERSION=8.7.0 - -if [[ "$(whoami)" != "root" ]] ; then - echo "You must run this script as root" - exit 1 -fi - -echo "* DANBOORU INSTALLATION SCRIPT" -echo "*" -echo "* This script will install all the necessary packages to run Danbooru on an" -echo "* Ubuntu server." -echo - -echo -n "* Enter the VLAN IP address for this server: " -read VLAN_IP_ADDR - -# Install packages -echo "* Installing packages..." - -apt-get update -apt-get -y install libssl-dev build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel redis git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix -apt-get -y install libpq-dev postgresql-client -apt-get -y install liblcms2-dev libjpeg-turbo8-dev libexpat1-dev libgif-dev libpng-dev libexif-dev - -# vrack specific stuff -apt-get -y install vlan -modprobe 8021q -echo "8021q" >> /etc/modules -vconfig add eno2 99 -ip addr add $VLAN_IP_ADDR/24 dev eno2.99 -ip link set up eno2.99 -curl -L -s $GITHUB_INSTALL_SCRIPTS/vrack-cfg.yaml -o /etc/netplan/01-netcfg.yaml - -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 -curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash - -apt-get update -apt-get -y install nodejs yarn -apt-get remove cmdtest - -# compile and install libvips (the version in apt is too old) -cd /tmp -wget -q https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz -tar xzf vips-$VIPS_VERSION.tar.gz -cd vips-$VIPS_VERSION -./configure --prefix=/usr -make install -ldconfig - -# Create user account -useradd -m danbooru -chsh -s /bin/bash danbooru -usermod -G danbooru,sudo danbooru - -# Set up Postgres -git clone https://github.com/r888888888/test_parser.git /tmp/test_parser -cd /tmp/test_parser -make install - -# Install rbenv -echo "* Installing rbenv..." -cd /tmp -sudo -u danbooru git clone git://github.com/sstephenson/rbenv.git ~danbooru/.rbenv -sudo -u danbooru touch ~danbooru/.bash_profile -echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~danbooru/.bash_profile -echo 'eval "$(rbenv init -)"' >> ~danbooru/.bash_profile -sudo -u danbooru mkdir -p ~danbooru/.rbenv/plugins -sudo -u danbooru git clone git://github.com/sstephenson/ruby-build.git ~danbooru/.rbenv/plugins/ruby-build -sudo -u danbooru bash -l -c "rbenv install $RUBY_VERSION" -sudo -u danbooru bash -l -c "rbenv global $RUBY_VERSION" - -# Install gems -echo "* Installing gems..." -sudo -u danbooru bash -l -c 'gem install --no-ri --no-rdoc bundler' - -# Setup danbooru account -echo "* Enter a new password for the danbooru account" -passwd danbooru - -echo "* Setting up SSH keys for the danbooru account" -sudo -u danbooru ssh-keygen -sudo -u danbooru cat ~danbooru/.ssh/id_rsa.pub >> ~danbooru/.ssh/authorized_keys - -echo "* TODO:" -echo "on kagamihara:" -echo "script/install/distribute_new_pubkey.sh" -echo -echo "on this server:" -echo "rsync -av kagamihara:/etc/nginx/nginx.conf /etc/nginx" -echo "rsync -av kagamihara:/etc/nginx/conf.d /etc/nginx" -echo "rsync -av kagamihara:/etc/nginx/sites-enabled /etc/nginx" -echo "rsync -av kagamihara:/etc/logrotate.d /etc/logrotate.d" diff --git a/script/install/distribute_new_pubkey.sh b/script/install/distribute_new_pubkey.sh deleted file mode 100644 index 52feaa245..000000000 --- a/script/install/distribute_new_pubkey.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -HOSTS="kagamihara shima saitou" - -echo "Enter new SSH pubkey: " -read $key - -for host in $HOSTS ; do - ssh danbooru@$host echo $key >> .ssh/authorized_keys -done diff --git a/script/install/nginx.service.d-local.conf b/script/install/nginx.service.d-local.conf deleted file mode 100644 index 01c1b16bc..000000000 --- a/script/install/nginx.service.d-local.conf +++ /dev/null @@ -1,2 +0,0 @@ -[Service] -LimitNOFILE=10000 diff --git a/script/install/nginx/conf.d/common.conf b/script/install/nginx/conf.d/common.conf deleted file mode 100644 index 677a14da7..000000000 --- a/script/install/nginx/conf.d/common.conf +++ /dev/null @@ -1,92 +0,0 @@ -ssl_protocols TLSv1 TLSv1.1 TLSv1.2; -ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; -ssl_prefer_server_ciphers on; -ssl_session_timeout 4h; -ssl_session_cache shared:SSL:20m; -ssl_session_tickets off; -ssl_stapling on; -ssl_stapling_verify on; -ssl_dhparam /etc/nginx/ssl/dhparam.pem; -resolver 8.8.8.8 8.8.4.4; - -root /var/www/danbooru/current/public; -index index.html; -access_log off; -error_log /var/www/danbooru/shared/log/server.error.log; -try_files $uri/index.html $uri.html $uri @app; -client_max_body_size 35m; -error_page 503 @maintenance; -error_page 404 /404.html; -error_page 500 502 503 504 /500.html; - -location /assets { - expires max; - break; -} - -location /data/preview { - expires max; - break; -} - -location /posts/mobile { - return 404; -} - -location /users { - limit_req zone=users burst=5; - limit_req_status 429; - try_files $uri @app_server; -} - -location /posts { - limit_req zone=posts burst=20; - limit_req_status 429; - try_files $uri @app_server; -} - -location /data { - valid_referers none *.donmai.us donmai.us ~\.google\. ~\.bing\. ~\.yahoo\.; - - if ($invalid_referer) { - return 403; - } - - rewrite ^/data/sample/__.+?__(.+) /data/sample/$1 last; - rewrite ^/data/__.+?__(.+) /data/$1 last; - - expires max; - break; -} - -location /maintenance.html { - expires 10; -} - -if (-f $document_root/maintenance.html) { - return 503; -} - -if ($http_user_agent ~ (WinHttp\.WinHttpRequest\.5) ) { - return 403; -} - -location @maintenance { - rewrite ^(.*)$ /maintenance.html last; -} - -location @app_server { - proxy_pass http://app_server; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_redirect off; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_buffer_size 128k; - proxy_buffers 4 256k; - proxy_busy_buffers_size 256k; -} - -location / { - try_files $uri @app_server; -} diff --git a/script/install/nginx/nginx.conf b/script/install/nginx/nginx.conf deleted file mode 100644 index f8d9b7e99..000000000 --- a/script/install/nginx/nginx.conf +++ /dev/null @@ -1,82 +0,0 @@ -user www-data; -worker_processes auto; -pid /var/run/nginx.pid; - -events { - use epoll; - worker_connections 10000; - multi_accept on; - accept_mutex on; -} - -http { - limit_req_zone $binary_remote_addr zone=users:10m rate=5r/s; - limit_req_zone $binary_remote_addr zone=posts:100m rate=10r/s; - - ## - # Basic Settings - ## - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 5; - types_hash_max_size 2048; - # server_tokens off; - - server_names_hash_bucket_size 128; - # server_name_in_redirect off; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - ## - # Logging Settings - ## - - access_log off; - error_log /var/log/nginx/error.log; - - ## - # Gzip Settings - ## - - gzip on; - gzip_disable "msie6"; - - gzip_http_version 1.1; - gzip_vary on; - gzip_comp_level 5; - gzip_proxied any; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/rss+xml text/javascript application/atom+xml; - - # curl https://www.cloudflare.com/ips-v4 | sort - set_real_ip_from 103.21.244.0/22; - set_real_ip_from 103.22.200.0/22; - set_real_ip_from 103.31.4.0/22; - set_real_ip_from 104.16.0.0/12; - set_real_ip_from 108.162.192.0/18; - set_real_ip_from 131.0.72.0/22; - set_real_ip_from 141.101.64.0/18; - set_real_ip_from 162.158.0.0/15; - set_real_ip_from 172.64.0.0/13; - set_real_ip_from 173.245.48.0/20; - set_real_ip_from 188.114.96.0/20; - set_real_ip_from 190.93.240.0/20; - set_real_ip_from 197.234.240.0/22; - set_real_ip_from 198.41.128.0/17; - set_real_ip_from 199.27.128.0/21; - - # curl https://www.cloudflare.com/ips-v4 | sort - set_real_ip_from 2400:cb00::/32; - set_real_ip_from 2606:4700::/32; - set_real_ip_from 2803:f800::/32; - set_real_ip_from 2405:b500::/32; - set_real_ip_from 2405:8100::/32; - set_real_ip_from 2a06:98c0::/29; - set_real_ip_from 2c0f:f248::/32; - - real_ip_header CF-Connecting-IP; - - include /etc/nginx/sites-enabled/*.conf; -} diff --git a/script/install/nginx/sites-enabled/danbooru.conf b/script/install/nginx/sites-enabled/danbooru.conf deleted file mode 100644 index 30d19cda3..000000000 --- a/script/install/nginx/sites-enabled/danbooru.conf +++ /dev/null @@ -1,68 +0,0 @@ -server { - listen 443 ssl http2 default_server; - server_name danbooru.donmai.us; - - ssl_certificate /etc/nginx/ssl/danbooru.chain.pem; - ssl_certificate_key /etc/nginx/ssl/danbooru.key; - - include /etc/nginx/conf.d/common.conf; -} - -server { - listen 443 ssl http2; - server_name safebooru.donmai.us; - - ssl_certificate /etc/nginx/ssl/safebooru.chain.pem; - ssl_certificate_key /etc/nginx/ssl/safebooru.key; - - include /etc/nginx/conf.d/common.conf; -} - -server { - listen 443 ssl http2; - server_name kagamihara.donmai.us; - - ssl_certificate /etc/letsencrypt/live/kagamihara.donmai.us/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/kagamihara.donmai.us/privkey.pem; # managed by Certbot - - include /etc/nginx/conf.d/common.conf; -} - -server { - listen 443 ssl http2; - server_name saitou.donmai.us; - - ssl_certificate /etc/letsencrypt/live/saitou.donmai.us/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/saitou.donmai.us/privkey.pem; # managed by Certbot - - include /etc/nginx/conf.d/common.conf; -} - -server { - listen 443 ssl http2; - server_name shima.donmai.us; - - ssl_certificate /etc/letsencrypt/live/shima.donmai.us/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/shima.donmai.us/privkey.pem; # managed by Certbot - - include /etc/nginx/conf.d/common.conf; -} - -# redirect HTTP to HTTPS. -server { - listen 80; - server_name safebooru.donmai.us danbooru.donmai.us kagamihara.donmai.us saitou.donmai.us shima.donmai.us; - return 301 https://$host$request_uri; -} - -# redirect donmai.us and www.donmai.us to danbooru.donmai.us. -server { - listen 80; - listen 443 ssl; - server_name donmai.us www.donmai.us; - return 301 https://danbooru.donmai.us$request_uri; -} - -upstream app_server { - server unix:/tmp/.unicorn.sock fail_timeout=0; -} diff --git a/script/install/vrack-cfg.yaml b/script/install/vrack-cfg.yaml deleted file mode 100644 index cbd7434a3..000000000 --- a/script/install/vrack-cfg.yaml +++ /dev/null @@ -1,10 +0,0 @@ -network: - version: 2 - renderer: networkd - ethernets: - eno2: {} - vlans: - eno2.99: - id: 99 - link: eno2 - addresses: [172.16.0.1] diff --git a/script/mock_services/README.md b/script/mock_services/README.md deleted file mode 100644 index 6f8f84e8a..000000000 --- a/script/mock_services/README.md +++ /dev/null @@ -1,7 +0,0 @@ -These are mocked services to be used for development purposes. - -- danbooru: port 3000 -- recommender: port 3001 -- iqdbs: port 3002 -- reportbooru: port 3003 - diff --git a/script/mock_services/iqdbs.rb b/script/mock_services/iqdbs.rb deleted file mode 100644 index a3cca08c3..000000000 --- a/script/mock_services/iqdbs.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'sinatra' -require 'json' -require_relative './mock_service_helper' - -set :port, 3002 - -configure do - POST_IDS = MockServiceHelper.fetch_post_ids -end - -get '/similar' do - content_type :json - POST_IDS[0..10].map {|x| {post_id: x}}.to_json -end diff --git a/script/mock_services/mock_service_helper.rb b/script/mock_services/mock_service_helper.rb deleted file mode 100644 index 2b259bc4c..000000000 --- a/script/mock_services/mock_service_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'socket' -require 'timeout' -require 'httparty' - -module MockServiceHelper - module_function - - DANBOORU_PORT = 3000 - - def fetch_post_ids - begin - s = TCPSocket.new("localhost", DANBOORU_PORT) - s.close - rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH - sleep 1 - retry - end - - json = HTTParty.get("http://localhost:#{DANBOORU_PORT}/posts.json?random=true&limit=10").body - return JSON.parse(json).map {|x| x["id"]} - end -end diff --git a/script/mock_services/recommender.rb b/script/mock_services/recommender.rb deleted file mode 100644 index cb5f2b317..000000000 --- a/script/mock_services/recommender.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'sinatra' -require 'json' -require_relative './mock_service_helper' - -set :port, 3001 - -configure do - POST_IDS = MockServiceHelper.fetch_post_ids -end - -get '/recommend/:user_id' do - content_type :json - POST_IDS[0..10].map {|x| [x, "1.000"]}.to_json -end - -get '/similar/:post_id' do - content_type :json - POST_IDS[0..6].map {|x| [x, "1.000"]}.to_json -end diff --git a/script/mock_services/reportbooru.rb b/script/mock_services/reportbooru.rb deleted file mode 100644 index 3aa40edb3..000000000 --- a/script/mock_services/reportbooru.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'sinatra' -require 'json' - -set :port, 3003 - -get '/missed_searches' do - content_type :text - return "abcdefg 10.0\nblahblahblah 20.0\n" -end - -get '/post_searches/rank' do - content_type :json - return [["abc", 100], ["def", 200]].to_json -end - -get '/reports/user_similarity' do - # todo -end - -post '/post_views' do - # todo -end diff --git a/test/.rubocop.yml b/test/.rubocop.yml new file mode 100644 index 000000000..f278ac69f --- /dev/null +++ b/test/.rubocop.yml @@ -0,0 +1,5 @@ +inherit_from: + - ../.rubocop.yml + +Metrics/ClassLength: + Enabled: false diff --git a/test/factories/tag.rb b/test/factories/tag.rb index 8a6b31d33..a2eb396e7 100644 --- a/test/factories/tag.rb +++ b/test/factories/tag.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory(:tag) do name {"#{FFaker::Name.first_name.downcase}#{rand(1000)}"} - post_count {0} + post_count { 100 } category {Tag.categories.general} factory(:artist_tag) do diff --git a/test/factories/user.rb b/test/factories/user.rb index 8eff8378d..577334f40 100644 --- a/test/factories/user.rb +++ b/test/factories/user.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory(:user, aliases: [:creator, :updater]) do - name { FFaker::Internet.user_name + $PID.to_s } + name { SecureRandom.uuid } password {"password"} default_image_size {"large"} level {20} diff --git a/test/files/test-empty.bin b/test/files/test-empty.bin new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/artists_controller_test.rb b/test/functional/artists_controller_test.rb index 2b24f3cc7..076d8fd41 100644 --- a/test/functional/artists_controller_test.rb +++ b/test/functional/artists_controller_test.rb @@ -4,11 +4,8 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest def assert_artist_found(expected_artist, source_url = nil) if source_url get_auth artists_path(format: "json", search: { url_matches: source_url }), @user - if response.body =~ /Net::OpenTimeout/ - skip "Remote connection to #{source_url} failed" - return - end end + assert_response :success json = JSON.parse(response.body) assert_equal(1, json.size, "Testing URL: #{source_url}") @@ -17,10 +14,6 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest def assert_artist_not_found(source_url) get_auth artists_path(format: "json", search: { url_matches: source_url }), @user - if response.body =~ /Net::OpenTimeout/ - skip "Remote connection to #{source_url} failed" - return - end assert_response :success json = JSON.parse(response.body) @@ -54,6 +47,22 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest get artist_path(@artist.id) assert_response :success end + + should "show active wikis" do + as(@user) { create(:wiki_page, title: @artist.name) } + get artist_path(@artist.id) + + assert_response :success + assert_select ".artist-wiki", count: 1 + end + + should "not show deleted wikis" do + as(@user) { create(:wiki_page, title: @artist.name, is_deleted: true) } + get artist_path(@artist.id) + + assert_response :success + assert_select ".artist-wiki", count: 0 + end end context "new action" do @@ -126,6 +135,12 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "get the sitemap" do + get artists_path(format: :sitemap) + assert_response :success + assert_equal(Artist.count, response.parsed_body.css("urlset url loc").size) + end + context "when searching the index page" do should "find artists by name" do get artists_path(name: "masao", format: "json") diff --git a/test/functional/autocomplete_controller_test.rb b/test/functional/autocomplete_controller_test.rb new file mode 100644 index 000000000..51c064029 --- /dev/null +++ b/test/functional/autocomplete_controller_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +class AutocompleteControllerTest < ActionDispatch::IntegrationTest + context "Autocomplete controller" do + context "index action" do + setup do + create(:tag, name: "azur_lane") + end + + should "work for opensearch queries" do + get autocomplete_index_path(query: "azur", variant: "opensearch"), as: :json + assert_response :success + assert_equal(["azur", ["azur lane"]], response.parsed_body) + end + end + end +end diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb index 43e8c2770..b0c74dbe5 100644 --- a/test/functional/comments_controller_test.rb +++ b/test/functional/comments_controller_test.rb @@ -93,9 +93,21 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest assert_response :success end - should "render for atom feeds" do - get comments_path(format: "atom") - assert_response :success + context "for atom feeds" do + should "render" do + @comment = as(@user) { create(:comment, post: @post) } + get comments_path(format: "atom") + assert_response :success + end + + should "not show comments on restricted posts" do + @post.update!(is_banned: true) + @comment = as(@user) { create(:comment, post: @post) } + + get comments_path(format: "atom") + assert_response :success + assert_equal(0, response.parsed_body.css("entry").size) + end end end diff --git a/test/functional/explore/posts_controller_test.rb b/test/functional/explore/posts_controller_test.rb index 171235f32..f553bc594 100644 --- a/test/functional/explore/posts_controller_test.rb +++ b/test/functional/explore/posts_controller_test.rb @@ -28,17 +28,29 @@ module Explore end end + context "#viewed" do + should "render" do + mock_post_view_rankings(Date.today, [[@post.id, 100]]) + get viewed_explore_posts_path + assert_response :success + end + end + context "#searches" do should "render" do + mock_post_search_rankings(Date.today, [["1girl", 100], ["original", 50]]) get searches_explore_posts_path assert_response :success + assert_select "tbody tr", count: 2 end end context "#missed_searches" do should "render" do + mock_missed_search_rankings([["1girl", 100], ["original", 50]]) get missed_searches_explore_posts_path assert_response :success + assert_select "tbody tr", count: 2 end end end diff --git a/test/functional/forum_post_votes_controller_test.rb b/test/functional/forum_post_votes_controller_test.rb index 7143c038d..5d693d26b 100644 --- a/test/functional/forum_post_votes_controller_test.rb +++ b/test/functional/forum_post_votes_controller_test.rb @@ -36,6 +36,13 @@ class ForumPostVotesControllerTest < ActionDispatch::IntegrationTest assert_response 403 end end + + should "not allow creators to vote on their own BURs" do + assert_difference("ForumPostVote.count", 0) do + post_auth forum_post_votes_path(format: :js), @bulk_update_request.user, params: { forum_post_id: @forum_post.id, forum_post_vote: { score: 1 }} + assert_response 403 + end + end end context "destroy action" do diff --git a/test/functional/forum_topics_controller_test.rb b/test/functional/forum_topics_controller_test.rb index e8368ff08..4b3a0e2e4 100644 --- a/test/functional/forum_topics_controller_test.rb +++ b/test/functional/forum_topics_controller_test.rb @@ -114,6 +114,12 @@ class ForumTopicsControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "render for a sitemap" do + get forum_topics_path(format: :sitemap) + assert_response :success + assert_equal(ForumTopic.count, response.parsed_body.css("urlset url loc").size) + end + context "with private topics" do should "not show private topics to unprivileged users" do as(@user) { @topic2.update!(min_level: User::Levels::MODERATOR) } diff --git a/test/functional/iqdb_queries_controller_test.rb b/test/functional/iqdb_queries_controller_test.rb index eb0f3d206..e9af6770d 100644 --- a/test/functional/iqdb_queries_controller_test.rb +++ b/test/functional/iqdb_queries_controller_test.rb @@ -3,61 +3,33 @@ require 'test_helper' class IqdbQueriesControllerTest < ActionDispatch::IntegrationTest context "The iqdb controller" do setup do - Danbooru.config.stubs(:iqdbs_server).returns("https://karasuma.donmai.us") @user = create(:user) - @posts = as(@user) { create_list(:post, 2) } + @post = as(@user) { create(:post) } end context "show action" do context "with a url parameter" do - setup do - @url = "https://google.com" - @params = { url: @url } - @mocked_response = [{ - "post" => @posts[0], - "post_id" => @posts[0].id, - "score" => 1 - }] - end - should "render a response" do - IqdbProxy.expects(:query).returns(@mocked_response) - get_auth iqdb_queries_path, @user, as: :javascript, params: @params + @url = "https://google.com" + @matches = [{ "post_id" => @post.id, "width" => 128, "height" => 128, "score" => 95.0 }] + mock_iqdb_matches(@matches) + + get_auth iqdb_queries_path, @user, as: :javascript, params: { url: @url } assert_response :success - assert_select("#post_#{@posts[0].id}") + assert_select("#post_#{@post.id}") end end context "with a post_id parameter" do - setup do - @params = { post_id: @posts[0].id } - @url = @posts[0].preview_file_url - @mocked_response = [{ - "post" => @posts[0], - "post_id" => @posts[0].id, - "score" => 1 - }] - end - should "redirect to iqdbs" do - IqdbProxy.expects(:query).returns(@mocked_response) - get_auth iqdb_queries_path, @user, params: @params + @matches = [{ "post_id" => @post.id, "width" => 128, "height" => 128, "score" => 95.0 }] + mock_iqdb_matches(@matches) + + get_auth iqdb_queries_path, @user, params: { post_id: @post.id } assert_response :success - assert_select("#post_#{@posts[0].id}") - end - end - - context "with matches" do - setup do - json = @posts.map {|x| {"post_id" => x.id, "score" => 1}}.to_json - @params = { matches: json } - end - - should "render with matches" do - get_auth iqdb_queries_path, @user, params: @params - assert_response :success + assert_select("#post_#{@post.id}") end end end diff --git a/test/functional/mock_services_controller_test.rb b/test/functional/mock_services_controller_test.rb new file mode 100644 index 000000000..5f33cc691 --- /dev/null +++ b/test/functional/mock_services_controller_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class MockServicesControllerTest < ActionDispatch::IntegrationTest + context "The mock services controller" do + setup do + create(:post) + create(:tag) + end + + context "for all actions" do + should "work" do + paths = [ + mock_recommender_recommend_path(42), + mock_recommender_similar_path(42), + mock_reportbooru_missed_searches_path, + mock_reportbooru_post_searches_path, + mock_reportbooru_post_views_path, + mock_iqdbs_similar_path, + ] + + paths.each do |path| + get path + assert_response :success + end + end + end + end +end diff --git a/test/functional/pools_controller_test.rb b/test/functional/pools_controller_test.rb index abf1efe83..35b9e3fff 100644 --- a/test/functional/pools_controller_test.rb +++ b/test/functional/pools_controller_test.rb @@ -23,6 +23,13 @@ class PoolsControllerTest < ActionDispatch::IntegrationTest get pools_path, params: {:search => {:name_matches => @pool.name}} assert_response :success end + + should "render for a sitemap" do + get pools_path(format: :sitemap) + assert_response :success + assert_equal(Pool.count, response.parsed_body.css("urlset url loc").size) + end + end context "show action" do diff --git a/test/functional/post_versions_controller_test.rb b/test/functional/post_versions_controller_test.rb index a57914cff..521d99adb 100644 --- a/test/functional/post_versions_controller_test.rb +++ b/test/functional/post_versions_controller_test.rb @@ -41,6 +41,13 @@ class PostVersionsControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_equal @post.versions[1].id, response.parsed_body[0]["id"].to_i end + + should "list all versions for search[tag_matches]" do + get post_versions_path, as: :json, params: { search: { tag_matches: "tagme" }} + assert_response :success + assert_equal @post.versions[0].id, response.parsed_body[0]["id"].to_i + assert_equal 1, response.parsed_body.length + end end context "undo action" do diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index d488062f0..67b03454f 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -1,18 +1,76 @@ require "test_helper" class PostsControllerTest < ActionDispatch::IntegrationTest + def assert_canonical_url_equals(expected) + assert_equal(expected, response.parsed_body.css("link[rel=canonical]").attribute("href").value) + end + context "The posts controller" do setup do - PopularSearchService.stubs(:enabled?).returns(false) - @user = travel_to(1.month.ago) {create(:user)} @post = as(@user) { create(:post, tag_string: "aaaa") } end context "index action" do - should "render" do - get posts_path - assert_response :success + setup do + mock_post_search_rankings(Date.today, [["1girl", 100], ["original", 50]]) + create_list(:post, 2) + end + + context "when using sequential pagination" do + should "work with page=a0" do + get posts_path(page: "a0") + assert_response :success + assert_select ".post-preview", count: 3 + assert_select "#paginator-prev", count: 0 + assert_select "#paginator-next", count: 1 + end + + should "work with page=b0" do + get posts_path(page: "b0") + assert_response :success + assert_select ".post-preview", count: 0 + assert_select "#paginator-prev", count: 0 + assert_select "#paginator-next", count: 0 + end + + should "work with page=b100000" do + get posts_path(page: "b100000") + assert_response :success + assert_select ".post-preview", count: 3 + assert_select "#paginator-prev", count: 1 + assert_select "#paginator-next", count: 0 + end + + should "work with page=a100000" do + get posts_path(page: "a100000") + assert_response :success + assert_select ".post-preview", count: 0 + assert_select "#paginator-prev", count: 0 + assert_select "#paginator-next", count: 0 + end + end + + context "for an empty search" do + should "render the first page" do + get root_path + assert_response :success + assert_canonical_url_equals(root_url(host: Danbooru.config.hostname)) + + get posts_path + assert_response :success + assert_canonical_url_equals(root_url(host: Danbooru.config.hostname)) + + get posts_path(page: 1) + assert_response :success + assert_canonical_url_equals(root_url(host: Danbooru.config.hostname)) + end + + should "render the second page" do + get posts_path(page: 2, limit: 1) + assert_response :success + assert_canonical_url_equals(posts_url(page: 2, host: Danbooru.config.hostname)) + end end context "with a single tag search" do @@ -20,6 +78,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest get posts_path, params: { tags: "does_not_exist" } assert_response :success assert_select "#show-excerpt-link", count: 0 + assert_canonical_url_equals(posts_url(tags: "does_not_exist", host: Danbooru.config.hostname)) end should "render for an artist tag" do @@ -32,6 +91,8 @@ class PostsControllerTest < ActionDispatch::IntegrationTest get posts_path, params: { tags: "bkub" } assert_response :success assert_select "#show-excerpt-link", count: 1, text: "Artist" + assert_select "#view-wiki-link", count: 0 + assert_select "#view-artist-link", count: 1 artist.update(is_banned: true) get posts_path, params: { tags: "bkub" } @@ -47,6 +108,8 @@ class PostsControllerTest < ActionDispatch::IntegrationTest get posts_path, params: { tags: "bkub" } assert_response :success assert_select "#show-excerpt-link", count: 1, text: "Wiki" + assert_select "#view-wiki-link", count: 1 + assert_select "#view-artist-link", count: 0 end should "render for a tag with a wiki page" do @@ -90,6 +153,13 @@ class PostsControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "show the wiki excerpt for a wiki page without a tag" do + as(@user) { create(:wiki_page, title: "no_tag") } + get posts_path(tags: "no_tag") + assert_select "#show-excerpt-link", count: 1 + assert_select "#excerpt", count: 1 + end + should "show a notice for a single tag search with a pending BUR" do create(:bulk_update_request, script: "create alias foo -> bar") get_auth posts_path(tags: "foo"), @user @@ -248,7 +318,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest get posts_path(format: :atom) assert_response :success - assert_select "entry", 1 + assert_select "entry", 3 end should "render with tags" do @@ -259,7 +329,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest end should "hide restricted posts" do - @post.update(is_banned: true) + Post.update_all(is_banned: true) get posts_path(format: :atom) assert_response :success @@ -267,6 +337,14 @@ class PostsControllerTest < ActionDispatch::IntegrationTest end end + context "with the .sitemap format" do + should "render" do + get posts_path(format: :sitemap) + assert_response :success + assert_equal(Post.count, response.parsed_body.css("urlset url loc").size) + end + end + context "with deleted posts" do setup do @post.update!(is_deleted: true) @@ -304,12 +382,52 @@ class PostsControllerTest < ActionDispatch::IntegrationTest end end + context "with banned paid_reward posts" do + setup do + as(@user) { @post.update!(tag_string: "paid_reward", is_banned: true) } + end + + should "show banned paid_rewards to approvers" do + get_auth posts_path, create(:approver) + assert_response :success + assert_select "#post_#{@post.id}", 1 + end + + should "not show banned paid_rewards to non-approvers" do + get_auth posts_path, create(:gold_user) + assert_response :success + assert_select "#post_#{@post.id}", 0 + end + end + context "in safe mode" do should "not include the rating:s tag in the page title" do get posts_path(tags: "1girl", safe_mode: true) assert_select "title", text: "1girl Art | Safebooru" end end + + context "for a search that times out" do + context "during numbered pagination" do + should "show the search timeout error page" do + Post::const_get(:ActiveRecord_Relation).any_instance.stubs(:records).raises(ActiveRecord::QueryCanceled) + + get posts_path(page: "1") + assert_response 500 + assert_select "h1", text: "Search Timeout" + end + end + + context "during sequential pagination" do + should "show the search timeout error page" do + Post::const_get(:ActiveRecord_Relation).any_instance.stubs(:records).raises(ActiveRecord::QueryCanceled) + + get posts_path(page: "a0") + assert_response 500 + assert_select "h1", text: "Search Timeout" + end + end + end end context "show_seq action" do diff --git a/test/functional/saved_searches_controller_test.rb b/test/functional/saved_searches_controller_test.rb index f5a703579..bc390dd08 100644 --- a/test/functional/saved_searches_controller_test.rb +++ b/test/functional/saved_searches_controller_test.rb @@ -5,6 +5,7 @@ class SavedSearchesControllerTest < ActionDispatch::IntegrationTest setup do @user = create(:user) @saved_search = create(:saved_search, user: @user) + SavedSearch.stubs(:redis).returns(MockRedis.new) end context "index action" do diff --git a/test/functional/static_controller_test.rb b/test/functional/static_controller_test.rb index 0fd2bd57b..faca87b71 100644 --- a/test/functional/static_controller_test.rb +++ b/test/functional/static_controller_test.rb @@ -14,10 +14,14 @@ class StaticControllerTest < ActionDispatch::IntegrationTest end context "sitemap action" do - should "work" do - create_list(:post, 3) - get sitemap_path, as: :xml - assert_response :success + [Artist, ForumTopic, Pool, Post, Tag, User, WikiPage].each do |klass| + should "work for #{klass.model_name.plural}" do + as(create(:user)) { create_list(klass.model_name.singular.to_sym, 3) } + get sitemap_path(sitemap: klass.model_name.plural), as: :xml + + assert_response :success + assert_equal(1, response.parsed_body.css("sitemap loc").size) + end end end @@ -35,6 +39,13 @@ class StaticControllerTest < ActionDispatch::IntegrationTest end end + context "privacy_policy action" do + should "work" do + get privacy_policy_path + assert_response :success + end + end + context "not_found action" do should "work" do get "/qwoiqogieqg" @@ -62,4 +73,11 @@ class StaticControllerTest < ActionDispatch::IntegrationTest assert_response :success end end + + context "opensearch action" do + should "work" do + get opensearch_path, as: :xml + assert_response :success + end + end end diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb index 83b01a1a9..91eea376d 100644 --- a/test/functional/tags_controller_test.rb +++ b/test/functional/tags_controller_test.rb @@ -20,18 +20,10 @@ class TagsControllerTest < ActionDispatch::IntegrationTest assert_response :success end - context "with search parameters" do - should "render" do - get tags_path, params: {:search => {:name_matches => "touhou"}} - assert_response :success - end - - should "work for search[fuzzy_name_matches]" do - get tags_path, as: :json, params: { search: { fuzzy_name_matches: "touhuo", order: "similarity" }} - - assert_response :success - assert_equal "touhou", response.parsed_body.first["name"] - end + should "render for a sitemap" do + get tags_path(format: :sitemap) + assert_response :success + assert_equal(Tag.count, response.parsed_body.css("urlset url loc").size) end context "with blank search parameters" do @@ -40,6 +32,32 @@ class TagsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to tags_path(search: { name: "touhou" }) end end + + context "searching" do + setup do + as(@user) do + @miku = create(:tag, name: "hatsune_miku", category: Tag.categories.character) + @wokada = create(:tag, name: "wokada", category: Tag.categories.artist) + @vocaloid = create(:tag, name: "vocaloid", category: Tag.categories.copyright) + @empty = create(:tag, name: "empty", post_count: 0) + + create(:tag_alias, antecedent_name: "miku", consequent_name: "hatsune_miku") + create(:wiki_page, title: "hatsune_miku") + create(:artist, name: "wokada") + end + end + + should respond_to_search(name_matches: "hatsune_miku").with { @miku } + should respond_to_search(name_normalize: "HATSUNE_MIKU ").with { @miku } + should respond_to_search(name_or_alias_matches: "miku").with { @miku } + should respond_to_search(fuzzy_name_matches: "miku_hatsune", order: "similarity").with { @miku } + should respond_to_search(name: "empty", hide_empty: "true").with { [] } + should respond_to_search(name: "empty", hide_empty: "false").with { [@empty] } + should respond_to_search(name: "wokada", has_artist: "true").with { @wokada } + should respond_to_search(name: "hatsune_miku", has_artist: "false").with { @miku } + should respond_to_search(name: "hatsune_miku", has_wiki: "true").with { @miku } + should respond_to_search(name: "vocaloid", has_wiki: "false").with { @vocaloid } + end end context "autocomplete action" do diff --git a/test/functional/uploads_controller_test.rb b/test/functional/uploads_controller_test.rb index 9df37a1b4..e5fcc445c 100644 --- a/test/functional/uploads_controller_test.rb +++ b/test/functional/uploads_controller_test.rb @@ -1,15 +1,31 @@ require 'test_helper' class UploadsControllerTest < ActionDispatch::IntegrationTest - def assert_uploaded(file_path, user, **upload_params) - file = Rack::Test::UploadedFile.new("#{Rails.root}/#{file_path}") + def self.should_upload_successfully(source) + should "upload successfully from #{source}" do + assert_successful_upload(source, user: create(:user, created_at: 1.month.ago)) + end + end - assert_difference(["Upload.count", "Post.count"]) do - post_auth uploads_path, user, params: { upload: { file: file, **upload_params }} - assert_redirected_to Upload.last + def assert_successful_upload(source_or_file_path, user: @user, **params) + if source_or_file_path =~ %r{\Ahttps?://}i + source = { source: source_or_file_path } + else + file = Rack::Test::UploadedFile.new(Rails.root.join(source_or_file_path)) + source = { file: file } end - Upload.last + assert_difference(["Upload.count"]) do + post_auth uploads_path, user, params: { upload: { tag_string: "abc", rating: "e", **source, **params }} + end + + upload = Upload.last + assert_response :redirect + assert_redirected_to upload + assert_equal("completed", upload.status) + assert_equal(Post.last, upload.post) + assert_equal(upload.post.md5, upload.md5) + upload end context "The uploads controller" do @@ -18,6 +34,17 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest mock_iqdb_service! end + context "image proxy action" do + should "work" do + url = "https://i.pximg.net/img-original/img/2017/11/21/17/06/44/65985331_p0.png" + get_auth image_proxy_uploads_path, @user, params: { url: url } + + assert_response :success + assert_equal("image/png", response.media_type) + assert_equal(15_573, response.body.size) + end + end + context "batch action" do context "for twitter galleries" do should "render" do @@ -258,33 +285,84 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest end end + context "when the upload is tagged banned_artist" do + should "autoban the post" do + upload = assert_successful_upload("test/files/test.jpg", tag_string: "banned_artist") + assert_equal(true, upload.post.is_banned?) + end + end + + context "when the upload is tagged paid_reward" do + should "autoban the post" do + upload = assert_successful_upload("test/files/test.jpg", tag_string: "paid_reward") + assert_equal(true, upload.post.is_banned?) + end + end + context "uploading a file from your computer" do - should "work for a jpeg file" do - upload = assert_uploaded("test/files/test.jpg", @user, tag_string: "aaa", rating: "e", source: "aaa") + should_upload_successfully("test/files/test.jpg") + should_upload_successfully("test/files/test.png") + should_upload_successfully("test/files/test-static-32x32.gif") + should_upload_successfully("test/files/test-animated-86x52.gif") + should_upload_successfully("test/files/test-300x300.mp4") + should_upload_successfully("test/files/test-512x512.webm") + should_upload_successfully("test/files/compressed.swf") + end - assert_equal("jpg", upload.post.file_ext) - assert_equal("aaa", upload.post.source) - assert_equal(500, upload.post.image_width) - assert_equal(335, upload.post.image_height) - end + context "uploading a file from a source" do + should_upload_successfully("https://www.artstation.com/artwork/04XA4") + should_upload_successfully("https://dantewontdie.artstation.com/projects/YZK5q") + should_upload_successfully("https://cdna.artstation.com/p/assets/images/images/006/029/978/large/amama-l-z.jpg") - should "work for a webm file" do - upload = assert_uploaded("test/files/test-512x512.webm", @user, tag_string: "aaa", rating: "e", source: "aaa") + should_upload_successfully("https://www.deviantart.com/aeror404/art/Holiday-Elincia-424551484") + should_upload_successfully("https://noizave.deviantart.com/art/test-no-download-697415967") + should_upload_successfully("https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/intermediary/f/8b472d70-a0d6-41b5-9a66-c35687090acc/d23jbr4-8a06af02-70cb-46da-8a96-42a6ba73cdb4.jpg/v1/fill/w_786,h_1017,q_70,strp/silverhawks_quicksilver_by_edsfox_d23jbr4-pre.jpg") - assert_equal("webm", upload.post.file_ext) - assert_equal("aaa", upload.post.source) - assert_equal(512, upload.post.image_width) - assert_equal(512, upload.post.image_height) - end + should_upload_successfully("https://www.hentai-foundry.com/pictures/user/Afrobull/795025/kuroeda") + should_upload_successfully("https://pictures.hentai-foundry.com/a/Afrobull/795025/Afrobull-795025-kuroeda.png") - should "work for a flash file" do - upload = assert_uploaded("test/files/compressed.swf", @user, tag_string: "aaa", rating: "e", source: "aaa") + should_upload_successfully("https://yande.re/post/show/482880") + should_upload_successfully("https://files.yande.re/image/7ecfdead705d7b956b26b1d37b98d089/yande.re%20482880.jpg") - assert_equal("swf", upload.post.file_ext) - assert_equal("aaa", upload.post.source) - assert_equal(607, upload.post.image_width) - assert_equal(756, upload.post.image_height) - end + should_upload_successfully("https://konachan.com/post/show/270916") + should_upload_successfully("https://konachan.com/image/ca12cdb79a66d242e95a6f958341bf05/Konachan.com%20-%20270916.png") + + should_upload_successfully("http://lohas.nicoseiga.jp/o/910aecf08e542285862954017f8a33a8c32a8aec/1433298801/4937663") + should_upload_successfully("http://seiga.nicovideo.jp/seiga/im4937663") + should_upload_successfully("https://seiga.nicovideo.jp/image/source/9146749") + should_upload_successfully("https://seiga.nicovideo.jp/watch/mg389884") + should_upload_successfully("https://dic.nicovideo.jp/oekaki/52833.png") + should_upload_successfully("https://lohas.nicoseiga.jp/o/971eb8af9bbcde5c2e51d5ef3a2f62d6d9ff5552/1589933964/3583893") + should_upload_successfully("http://lohas.nicoseiga.jp/priv/3521156?e=1382558156&h=f2e089256abd1d453a455ec8f317a6c703e2cedf") + should_upload_successfully("http://lohas.nicoseiga.jp/priv/b80f86c0d8591b217e7513a9e175e94e00f3c7a1/1384936074/3583893") + should_upload_successfully("http://lohas.nicoseiga.jp/material/5746c5/4459092") + # XXX should_upload_successfully("https://dcdn.cdn.nimg.jp/priv/62a56a7f67d3d3746ae5712db9cac7d465f4a339/1592186183/10466669") + # XXX should_upload_successfully("https://dcdn.cdn.nimg.jp/nicoseiga/lohas/o/8ba0a9b2ea34e1ef3b5cc50785bd10cd63ec7e4a/1592187477/10466669") + + should_upload_successfully("http://nijie.info/view.php?id=213043") + should_upload_successfully("https://nijie.info/view_popup.php?id=213043") + should_upload_successfully("https://pic.nijie.net/03/nijie_picture/728995_20170505014820_0.jpg") + + should_upload_successfully("https://pawoo.net/web/statuses/1202176") + should_upload_successfully("https://img.pawoo.net/media_attachments/files/000/128/953/original/4c0a06087b03343f.png") + + should_upload_successfully("https://www.pixiv.net/en/artworks/64476642") + should_upload_successfully("https://i.pximg.net/img-original/img/2017/08/18/00/09/21/64476642_p0.jpg") + + should_upload_successfully("https://noizave.tumblr.com/post/162206271767") + should_upload_successfully("https://media.tumblr.com/3bbfcbf075ddf969c996641b264086fd/tumblr_os2buiIOt51wsfqepo1_1280.png") + + should_upload_successfully("https://twitter.com/noizave/status/875768175136317440") + should_upload_successfully("https://pbs.twimg.com/media/DCdZ_FhUIAAYKFN?format=jpg&name=medium") + should_upload_successfully("https://pbs.twimg.com/profile_banners/1225702850002468864/1588597370/1500x500") + # XXX should_upload_successfully("https://video.twimg.com/tweet_video/EWHWVrmVcAAp4Vw.mp4") + + should_upload_successfully("https://www.weibo.com/5501756072/J2UNKfbqV") + should_upload_successfully("https://wx1.sinaimg.cn/mw690/0060kO5aly1gezsyt5xvhj30ok0sgtc9.jpg") + + should_upload_successfully("https://art.ngfiles.com/images/1254000/1254722_natthelich_pandora.jpg") + should_upload_successfully("https://art.ngfiles.com/comments/57000/iu_57615_7115981.jpg") + should_upload_successfully("https://www.newgrounds.com/art/view/puddbytes/costanza-at-bat") end end end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index e0454e074..c342a98a0 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -12,6 +12,12 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "render for a sitemap" do + get users_path(format: :sitemap) + assert_response :success + assert_equal(User.count, response.parsed_body.css("urlset url loc").size) + end + should "list all users for /users?name=" do get users_path, params: { name: @user.name } assert_redirected_to(@user) @@ -81,6 +87,34 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_equal(false, xml["user"]["enable_safe_mode"]) end + + context "for a tooltip" do + setup do + @banned = create(:banned_user) + @admin = create(:admin_user) + @member = create(:user) + @feedback = create(:user_feedback, user: @member, category: :positive) + end + + should "render for a banned user" do + get_auth user_path(@banned, variant: "tooltip"), @user + + assert_response :success + end + + should "render for a member" do + get_auth user_path(@member, variant: "tooltip"), @user + assert_response :success + + get_auth user_path(@member, variant: "tooltip"), @admin + assert_response :success + end + + should "render for an admin" do + get_auth user_path(@admin, variant: "tooltip"), @user + assert_response :success + end + end end context "profile action" do @@ -131,6 +165,11 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_no_enqueued_emails end + should "not allow logged in users to create a new account" do + post_auth users_path, @user, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }} + assert_response 403 + end + should "create a user with a valid email" do post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1", email: "webmaster@danbooru.donmai.us" }} diff --git a/test/functional/wiki_pages_controller_test.rb b/test/functional/wiki_pages_controller_test.rb index 119483201..713b28e1e 100644 --- a/test/functional/wiki_pages_controller_test.rb +++ b/test/functional/wiki_pages_controller_test.rb @@ -10,8 +10,11 @@ class WikiPagesControllerTest < ActionDispatch::IntegrationTest context "index action" do setup do as(@user) do - @wiki_page_abc = create(:wiki_page, :title => "abc") - @wiki_page_def = create(:wiki_page, :title => "def") + @tagme = create(:wiki_page, title: "tagme") + @deleted = create(:wiki_page, title: "deleted", is_deleted: true) + @vocaloid = create(:wiki_page, title: "vocaloid") + @miku = create(:wiki_page, title: "hatsune_miku", other_names: ["初音ミク"], body: "miku is a [[vocaloid]]") + create(:tag, name: "hatsune_miku", category: Tag.categories.character) end end @@ -20,22 +23,30 @@ class WikiPagesControllerTest < ActionDispatch::IntegrationTest assert_response :success end - should "list all wiki_pages (with search)" do - get wiki_pages_path, params: {:search => {:title => "abc"}} + should "render for a sitemap" do + get wiki_pages_path(format: :sitemap) assert_response :success - assert_select "tr td:first-child", text: "abc" - end - - should "list wiki_pages without tags with order=post_count" do - get wiki_pages_path, params: {:search => {:title => "abc", :order => "post_count"}} - assert_response :success - assert_select "tr td:first-child", text: "abc" + assert_equal(WikiPage.count, response.parsed_body.css("urlset url loc").size) end should "redirect the legacy title param to the show page" do - get wiki_pages_path(title: "abc") - assert_redirected_to wiki_pages_path(search: { title_normalize: "abc" }, redirect: true) + get wiki_pages_path(title: "tagme") + assert_redirected_to wiki_pages_path(search: { title_normalize: "tagme" }, redirect: true) end + + should respond_to_search(title: "tagme").with { @tagme } + should respond_to_search(title: "tagme", order: "post_count").with { @tagme } + should respond_to_search(title_normalize: "TAGME ").with { @tagme } + + should respond_to_search(tag: { category: Tag.categories.character }).with { @miku } + should respond_to_search(hide_deleted: "true").with { [@miku, @vocaloid, @tagme] } + should respond_to_search(linked_to: "vocaloid").with { @miku } + should respond_to_search(not_linked_to: "vocaloid").with { [@vocaloid, @deleted, @tagme] } + + should respond_to_search(other_names_match: "初音ミク").with { @miku } + should respond_to_search(other_names_match: "初*").with { @miku } + should respond_to_search(other_names_present: "true").with { @miku } + should respond_to_search(other_names_present: "false").with { [@vocaloid, @deleted, @tagme] } end context "search action" do diff --git a/test/test_helper.rb b/test/test_helper.rb index e395a6195..0959a8ccd 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -43,17 +43,15 @@ class ActiveSupport::TestCase setup do Socket.stubs(:gethostname).returns("www.example.com") - mock_popular_search_service! - mock_missed_search_service! - WebMock.allow_net_connect! - storage_manager = StorageManager::Local.new(base_dir: Dir.mktmpdir("uploads-test-storage-")) + @temp_dir = Dir.mktmpdir("danbooru-temp-") + storage_manager = StorageManager::Local.new(base_dir: @temp_dir) Danbooru.config.stubs(:storage_manager).returns(storage_manager) Danbooru.config.stubs(:backup_storage_manager).returns(StorageManager::Null.new) end teardown do - FileUtils.rm_rf(Danbooru.config.storage_manager.base_dir) + FileUtils.rm_rf(@temp_dir) Cache.clear end @@ -63,7 +61,11 @@ class ActiveSupport::TestCase end class ActionDispatch::IntegrationTest + extend ControllerHelper + register_encoder :xml, response_parser: ->(body) { Nokogiri.XML(body) } + register_encoder :atom, response_parser: ->(body) { Nokogiri.XML(body) } + register_encoder :html, response_parser: ->(body) { Nokogiri.HTML5(body) } def method_authenticated(method_name, url, user, **options) post session_path, params: { name: user.name, password: user.password } diff --git a/test/test_helpers/controller_helper.rb b/test/test_helpers/controller_helper.rb new file mode 100644 index 000000000..716bfaa97 --- /dev/null +++ b/test/test_helpers/controller_helper.rb @@ -0,0 +1,47 @@ +module ControllerHelper + # A custom Shoulda matcher that tests that a controller's index endpoint + # responds to a search correctly. See https://thoughtbot.com/blog/shoulda-matchers. + # + # Usage: + # + # # Tests that `/tags.json?search[name]=touhou` returns the `touhou` tag. + # subject { TagsController } + # setup { @touhou = create(:tag, name: "touhou") } + # should respond_to_search(name: "touhou").with { @touhou } + # + def respond_to_search(search_params) + RespondToSearchMatcher.new(search_params) + end + + class RespondToSearchMatcher < Struct.new(:params) + def description + "should respond to a search for #{params}" + end + + def matches?(subject, &block) + search_params = { search: params } + expected_items = @test_case.instance_eval(&@expected) + + @test_case.instance_eval do + # calls e.g. "wiki_pages_path" if we're in WikiPagesControllerTest. + index_url = send("#{subject.controller_path}_path") + get index_url, as: :json, params: search_params + + expected_ids = Array(expected_items).map(&:id) + responded_ids = response.parsed_body.map { |item| item["id"] } + + assert_response :success + assert_equal(expected_ids, responded_ids) + end + end + + def with(&block) + @expected = block + self + end + + def in_context(test_case) + @test_case = test_case + end + end +end diff --git a/test/test_helpers/download_test_helper.rb b/test/test_helpers/download_test_helper.rb index 72805cf9b..68e14abc6 100644 --- a/test/test_helpers/download_test_helper.rb +++ b/test/test_helpers/download_test_helper.rb @@ -1,10 +1,8 @@ module DownloadTestHelper def assert_downloaded(expected_filesize, source, referer = nil) - download = Downloads::File.new(source, referer) - tempfile, strategy = download.download! - assert_equal(expected_filesize, tempfile.size, "Tested source URL: #{source}") - rescue Net::OpenTimeout - skip "Remote connection to #{source} failed" + strategy = Sources::Strategies.find(source, referer) + file = strategy.download_file! + assert_equal(expected_filesize, file.size, "Tested source URL: #{source}") end def assert_rewritten(expected_source, test_source, test_referer = nil) @@ -16,19 +14,4 @@ module DownloadTestHelper def assert_not_rewritten(source, referer = nil) assert_rewritten(source, source, referer) end - - def assert_http_exists(url, headers: {}) - res = HTTParty.head(url, Danbooru.config.httparty_options.deep_merge(headers: headers)) - assert_equal(true, res.success?) - end - - def assert_http_status(code, url, headers: {}) - res = HTTParty.head(url, Danbooru.config.httparty_options.deep_merge(headers: headers)) - assert_equal(code, res.code) - end - - def assert_http_size(size, url, headers: {}) - res = HTTParty.head(url, Danbooru.config.httparty_options.deep_merge(headers: headers)) - assert_equal(size, res.content_length) - end end diff --git a/test/test_helpers/iqdb_test_helper.rb b/test/test_helpers/iqdb_test_helper.rb index 1feeffabc..4b52eade7 100644 --- a/test/test_helpers/iqdb_test_helper.rb +++ b/test/test_helpers/iqdb_test_helper.rb @@ -21,11 +21,9 @@ module IqdbTestHelper Danbooru.config.stubs(:iqdbs_server).returns("http://localhost:3004") end - def mock_iqdb_matches!(post_or_source, matches) - source = post_or_source.is_a?(Post) ? post_or_source.preview_file_url : post_or_source - url = "http://localhost:3004/similar?key=hunter2&url=#{CGI.escape source}&ref" - body = matches.map { |post| { post_id: post.id } }.to_json - - stub_request(:get, url).to_return(body: body) + def mock_iqdb_matches(matches) + Danbooru.config.stubs(:iqdbs_server).returns("http://localhost:3004") + response = HTTP::Response.new(status: 200, body: matches.to_json, headers: { "Content-Type": "application/json" }, version: "1.1") + HTTP::Client.any_instance.stubs(:post).returns(response) end end diff --git a/test/test_helpers/reportbooru_helper.rb b/test/test_helpers/reportbooru_helper.rb index 441df8216..3ec926202 100644 --- a/test/test_helpers/reportbooru_helper.rb +++ b/test/test_helpers/reportbooru_helper.rb @@ -1,12 +1,25 @@ module ReportbooruHelper - def mock_popular_search_service! - Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:3003") - stub_request(:get, "http://localhost:3003/post_searches/month?date=#{Date.today}").to_return(body: "kantai_collection 1000.0\ntouhou 500.0") - stub_request(:get, "http://localhost:3003/post_searches/day?date=#{Date.today}").to_return(body: "kantai_collection 1000.0\ntouhou 500.0") + def mock_request(url, method: :get, status: 200, body: nil, http: Danbooru::Http.any_instance, **options) + response = HTTP::Response.new(status: status, body: body, version: "1.1") + http.stubs(method).with(url, **options).returns(response) end - def mock_missed_search_service! - Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:3003") - stub_request(:get, "http://localhost:3003/missed_searches").to_return(body: "kantai_collection 1000.0\ntouhou 500.0") + def mock_post_search_rankings(date = Date.today, rankings) + Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:1234") + url = "http://localhost:1234/post_searches/rank?date=#{date}" + mock_request(url, body: rankings.to_json) + end + + def mock_missed_search_rankings(date = Date.today, rankings) + Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:1234") + url = "http://localhost:1234/missed_searches" + data = rankings.map { _1.join(" ") }.join("\n") + mock_request(url, body: data) + end + + def mock_post_view_rankings(date = Date.today, rankings) + Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:1234") + url = "http://localhost:1234/post_views/rank?date=#{date}" + mock_request(url, body: rankings.to_json) end end diff --git a/test/unit/artist_test.rb b/test/unit/artist_test.rb index b9dc3a90c..d82beccca 100644 --- a/test/unit/artist_test.rb +++ b/test/unit/artist_test.rb @@ -6,15 +6,11 @@ class ArtistTest < ActiveSupport::TestCase assert_equal(1, artists.size) assert_equal(expected_name, artists.first.name, "Testing URL: #{source_url}") - rescue Net::OpenTimeout, PixivApiClient::Error - skip "Remote connection failed for #{source_url}" end def assert_artist_not_found(source_url) artists = ArtistFinder.find_artists(source_url).to_a assert_equal(0, artists.size, "Testing URL: #{source_url}") - rescue Net::OpenTimeout - skip "Remote connection failed for #{source_url}" end context "An artist" do @@ -66,8 +62,8 @@ class ArtistTest < ActiveSupport::TestCase context "that has been banned" do setup do - @post = FactoryBot.create(:post, :tag_string => "aaa") @artist = FactoryBot.create(:artist, :name => "aaa") + @post = FactoryBot.create(:post, :tag_string => "aaa") @admin = FactoryBot.create(:admin_user) @artist.ban!(banner: @admin) @post.reload @@ -132,7 +128,8 @@ class ArtistTest < ActiveSupport::TestCase should "not allow invalid urls" do artist = FactoryBot.build(:artist, :url_string => "blah") assert_equal(false, artist.valid?) - assert_equal(["'blah' must begin with http:// or https:// "], artist.errors["urls.url"]) + assert_includes(artist.errors["urls.url"], "'blah' must begin with http:// or https:// ") + assert_includes(artist.errors["urls.url"], "'blah' has a hostname '' that does not contain a dot") end should "allow fixing invalid urls" do @@ -172,15 +169,11 @@ class ArtistTest < ActiveSupport::TestCase a2 = FactoryBot.create(:artist, :name => "subway", :url_string => "http://subway.com/x/test.jpg") a3 = FactoryBot.create(:artist, :name => "minko", :url_string => "https://minko.com/x/test.jpg") - begin - assert_artist_found("rembrandt", "http://rembrandt.com/x/test.jpg") - assert_artist_found("rembrandt", "http://rembrandt.com/x/another.jpg") - assert_artist_not_found("http://nonexistent.com/test.jpg") - assert_artist_found("minko", "https://minko.com/x/test.jpg") - assert_artist_found("minko", "http://minko.com/x/test.jpg") - rescue Net::OpenTimeout - skip "network failure" - end + assert_artist_found("rembrandt", "http://rembrandt.com/x/test.jpg") + assert_artist_found("rembrandt", "http://rembrandt.com/x/another.jpg") + assert_artist_not_found("http://nonexistent.com/test.jpg") + assert_artist_found("minko", "https://minko.com/x/test.jpg") + assert_artist_found("minko", "http://minko.com/x/test.jpg") end should "be case-insensitive to domains when finding matches by url" do @@ -414,9 +407,9 @@ class ArtistTest < ActiveSupport::TestCase end should "search on has_tag and return matches" do - post = FactoryBot.create(:post, tag_string: "bkub") bkub = FactoryBot.create(:artist, name: "bkub") none = FactoryBot.create(:artist, name: "none") + post = FactoryBot.create(:post, tag_string: "bkub") assert_equal(bkub.id, Artist.search(has_tag: "true").first.id) assert_equal(none.id, Artist.search(has_tag: "false").first.id) @@ -450,8 +443,8 @@ class ArtistTest < ActiveSupport::TestCase assert(Tag.exists?(name: "bkub", category: Tag.categories.artist)) end - should "change the tag to an artist tag if it was a gentag" do - tag = FactoryBot.create(:tag, name: "abc", category: Tag.categories.general) + should "change the tag to an artist tag if it was an empty gentag" do + tag = FactoryBot.create(:tag, name: "abc", category: Tag.categories.general, post_count: 0) artist = FactoryBot.create(:artist, name: "abc") assert_equal(Tag.categories.artist, tag.reload.category) @@ -468,7 +461,7 @@ class ArtistTest < ActiveSupport::TestCase context "when renaming" do should "change the new tag to an artist tag if it was a gentag" do - tag = FactoryBot.create(:tag, name: "def", category: Tag.categories.general) + tag = FactoryBot.create(:tag, name: "def", category: Tag.categories.general, post_count: 0) artist = FactoryBot.create(:artist, name: "abc") artist.update(name: "def") @@ -537,7 +530,7 @@ class ArtistTest < ActiveSupport::TestCase assert_equal("niceandcool", artist.name) assert_equal("nice_and_cool", artist.other_names_string) - assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/member.php?id=906442") + assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/users/906442") assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/stacc/niceandcool") end @@ -548,7 +541,7 @@ class ArtistTest < ActiveSupport::TestCase assert_equal("test_artist", artist.name) assert_equal("nice_and_cool niceandcool", artist.other_names_string) - assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/member.php?id=906442") + assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/users/906442") assert_includes(artist.urls.map(&:url), "https://www.pixiv.net/stacc/niceandcool") end end diff --git a/test/unit/artist_url_test.rb b/test/unit/artist_url_test.rb index 776554646..fc4f22ba4 100644 --- a/test/unit/artist_url_test.rb +++ b/test/unit/artist_url_test.rb @@ -24,10 +24,18 @@ class ArtistUrlTest < ActiveSupport::TestCase end should "disallow invalid urls" do - url = FactoryBot.build(:artist_url, url: "www.example.com") + urls = [ + FactoryBot.build(:artist_url, url: "www.example.com"), + FactoryBot.build(:artist_url, url: ":www.example.com"), + FactoryBot.build(:artist_url, url: "http://http://www.example.com"), + ] - assert_equal(false, url.valid?) - assert_match(/must begin with http/, url.errors.full_messages.join) + assert_equal(false, urls[0].valid?) + assert_match(/must begin with http/, urls[0].errors.full_messages.join) + assert_equal(false, urls[1].valid?) + assert_match(/is malformed/, urls[1].errors.full_messages.join) + assert_equal(false, urls[2].valid?) + assert_match(/that does not contain a dot/, urls[2].errors.full_messages.join) end should "always add a trailing slash when normalized" do diff --git a/test/unit/cloudflare_service_test.rb b/test/unit/cloudflare_service_test.rb index faced9207..cb9bc86be 100644 --- a/test/unit/cloudflare_service_test.rb +++ b/test/unit/cloudflare_service_test.rb @@ -1,5 +1,4 @@ require 'test_helper' -require 'webmock/minitest' class CloudflareServiceTest < ActiveSupport::TestCase def setup @@ -8,16 +7,11 @@ class CloudflareServiceTest < ActiveSupport::TestCase context "#purge_cache" do should "make calls to cloudflare's api" do - stub_request(:any, "api.cloudflare.com") - @cloudflare.purge_cache(["http://localhost/file.txt"]) + url = "http://www.example.com/file.jpg" + mock_request("https://api.cloudflare.com/client/v4/zones/123/purge_cache", method: :delete, json: { files: [url] }) - assert_requested(:delete, "https://api.cloudflare.com/client/v4/zones/123/purge_cache", times: 1) - end - end - - context "#ips" do - should "work" do - refute_empty(@cloudflare.ips) + response = @cloudflare.purge_cache([url]) + assert_equal(200, response.status) end end end diff --git a/test/unit/danbooru_http_test.rb b/test/unit/danbooru_http_test.rb new file mode 100644 index 000000000..4cc54ac29 --- /dev/null +++ b/test/unit/danbooru_http_test.rb @@ -0,0 +1,200 @@ +require 'test_helper' + +class DanbooruHttpTest < ActiveSupport::TestCase + context "Danbooru::Http" do + context "#get method" do + should "work for all basic methods" do + %i[get head put post delete].each do |method| + response = Danbooru::Http.send(method, "https://httpbin.org/status/200") + assert_equal(200, response.status) + end + end + + should "follow redirects" do + skip "Skipping test (https://github.com/postmanlabs/httpbin/issues/617)" + response = Danbooru::Http.get("https://httpbin.org/absolute-redirect/3") + assert_equal(200, response.status) + end + + should "fail if redirected too many times" do + skip "Skipping test (https://github.com/postmanlabs/httpbin/issues/617)" + response = Danbooru::Http.get("https://httpbin.org/absolute-redirect/10") + assert_equal(596, response.status) + end + + should "fail if the request takes too long to connect" do + response = Danbooru::Http.timeout(1).get("https://httpbin.org/delay/5") + assert_equal(597, response.status) + end + + should "fail if the request takes too long to download" do + # XXX should return status 597 instead + assert_raises(HTTP::TimeoutError) do + response = Danbooru::Http.timeout(1).get("https://httpbin.org/drip?duration=10&numbytes=10").flush + end + end + + should "return a 5xx error if the domain can't be resolved" do + response = Danbooru::Http.get("http://doesnotexist.donmai.us") + assert_equal(598, response.status) + end + + should "return a 5xx error if the SSL certificate is expired" do + response = Danbooru::Http.get("https://expired.badssl.com") + assert_equal(590, response.status) + end + + should "automatically decompress gzipped responses" do + response = Danbooru::Http.get("https://httpbin.org/gzip") + assert_equal(200, response.status) + assert_equal(true, response.parse["gzipped"]) + end + + should "automatically parse html responses" do + response = Danbooru::Http.get("https://httpbin.org/html") + assert_equal(200, response.status) + assert_instance_of(Nokogiri::HTML5::Document, response.parse) + assert_equal("Herman Melville - Moby-Dick", response.parse.css("h1").text) + end + + should "automatically parse xml responses" do + response = Danbooru::Http.get("https://httpbin.org/xml") + assert_equal(200, response.status) + assert_equal(true, response.parse[:slideshow].present?) + end + + should "track cookies between requests" do + http = Danbooru::Http.use(:session) + + resp1 = http.get("https://httpbin.org/cookies/set/abc/1") + resp2 = http.get("https://httpbin.org/cookies/set/def/2") + resp3 = http.get("https://httpbin.org/cookies") + assert_equal({ abc: "1", def: "2" }, resp3.parse["cookies"].symbolize_keys) + + resp4 = http.cookies(def: 3, ghi: 4).get("https://httpbin.org/cookies") + assert_equal({ abc: "1", def: "3", ghi: "4" }, resp4.parse["cookies"].symbolize_keys) + end + end + + context "cache feature" do + should "cache multiple requests to the same url" do + http = Danbooru::Http.cache(1.hour) + + response1 = http.get("https://httpbin.org/uuid") + assert_equal(200, response1.status) + + response2 = http.get("https://httpbin.org/uuid") + assert_equal(200, response2.status) + assert_equal(response2.to_s, response1.to_s) + end + + should "cache cookies correctly" do + http = Danbooru::Http.cache(1.hour) + + resp1 = http.get("https://httpbin.org/cookies") + resp2 = http.get("https://httpbin.org/cookies/set/abc/1") + resp3 = http.get("https://httpbin.org/cookies/set/def/2") + resp4 = http.get("https://httpbin.org/cookies") + + assert_equal(200, resp1.status) + assert_equal(200, resp2.status) + assert_equal(200, resp3.status) + assert_equal(200, resp4.status) + + assert_equal({}, resp1.parse["cookies"].symbolize_keys) + assert_equal({ abc: "1" }, resp2.parse["cookies"].symbolize_keys) + assert_equal({ abc: "1", def: "2" }, resp3.parse["cookies"].symbolize_keys) + assert_equal({ abc: "1", def: "2" }, resp4.parse["cookies"].symbolize_keys) + end + end + + context "retriable feature" do + should "retry immediately if no Retry-After header is sent" do + response_429 = ::HTTP::Response.new(status: 429, version: "1.1", body: "") + response_200 = ::HTTP::Response.new(status: 200, version: "1.1", body: "") + HTTP::Client.any_instance.expects(:perform).times(2).returns(response_429, response_200) + + response = Danbooru::Http.use(:retriable).get("https://httpbin.org/status/429") + assert_equal(200, response.status) + end + + should "retry if the Retry-After header is an integer" do + response_503 = ::HTTP::Response.new(status: 503, version: "1.1", headers: { "Retry-After": "1" }, body: "") + response_200 = ::HTTP::Response.new(status: 200, version: "1.1", body: "") + HTTP::Client.any_instance.expects(:perform).times(2).returns(response_503, response_200) + + response = Danbooru::Http.use(:retriable).get("https://httpbin.org/status/503") + assert_equal(200, response.status) + end + + should "retry if the Retry-After header is a date" do + response_503 = ::HTTP::Response.new(status: 503, version: "1.1", headers: { "Retry-After": 2.seconds.from_now.httpdate }, body: "") + response_200 = ::HTTP::Response.new(status: 200, version: "1.1", body: "") + HTTP::Client.any_instance.expects(:perform).times(2).returns(response_503, response_200) + + response = Danbooru::Http.use(:retriable).get("https://httpbin.org/status/503") + assert_equal(200, response.status) + end + end + + context "spoof referrer feature" do + should "spoof the referer" do + response = Danbooru::Http.use(:spoof_referrer).get("https://httpbin.org/anything") + + assert_equal(200, response.status) + assert_equal("https://httpbin.org", response.parse.dig("headers", "Referer")) + end + end + + context "unpolish cloudflare feature" do + should "return the original image for polished images" do + url = "https://cdnb.artstation.com/p/assets/images/images/025/273/307/4k/atey-ghailan-a-sage-keyart-s-ch-04-outlined-1.jpg?1585246642" + response = Danbooru::Http.use(:unpolish_cloudflare).get(url) + + assert_equal(200, response.status) + assert_equal(622_594, response.content_length) + end + end + + context "#download method" do + should "download files" do + response, file = Danbooru::Http.download_media("https://httpbin.org/bytes/1000") + + assert_equal(200, response.status) + assert_equal(1000, file.size) + end + + should "follow redirects when downloading files" do + skip "Skipping test (https://github.com/postmanlabs/httpbin/issues/617)" + response, file = Danbooru::Http.download_media("https://httpbin.org/redirect-to?url=https://httpbin.org/bytes/1000") + + assert_equal(200, response.status) + assert_equal(1000, file.size) + end + + should "fail if the url points to a private IP" do + assert_raises(Danbooru::Http::DownloadError) do + Danbooru::Http.public_only.download_media("https://127.0.0.1.xip.io") + end + end + + should "fail if the url redirects to a private IP" do + assert_raises(Danbooru::Http::DownloadError) do + Danbooru::Http.public_only.download_media("https://httpbin.org/redirect-to?url=https://127.0.0.1.xip.io") + end + end + + should "fail if a download is too large" do + assert_raises(Danbooru::Http::FileTooLargeError) do + response, file = Danbooru::Http.max_size(500).download_media("https://httpbin.org/bytes/1000") + end + end + + should "fail if a streaming download is too large" do + assert_raises(Danbooru::Http::FileTooLargeError) do + response, file = Danbooru::Http.max_size(500).download_media("https://httpbin.org/stream-bytes/1000") + end + end + end + end +end diff --git a/test/unit/downloads/art_station_test.rb b/test/unit/downloads/art_station_test.rb index 0ebacd585..47e4609c6 100644 --- a/test/unit/downloads/art_station_test.rb +++ b/test/unit/downloads/art_station_test.rb @@ -3,53 +3,27 @@ require 'test_helper' module Downloads class ArtStationTest < ActiveSupport::TestCase context "a download for a (small) artstation image" do - setup do - @asset = "https://cdnb3.artstation.com/p/assets/images/images/003/716/071/small/aoi-ogata-hate-city.jpg?1476754974" - @download = Downloads::File.new(@asset) - end - should "download the /4k/ image instead" do - file, strategy = @download.download! - assert_equal(1_880_910, ::File.size(file.path)) + assert_downloaded(1_816_438, "https://cdnb3.artstation.com/p/assets/images/images/003/716/071/small/aoi-ogata-hate-city.jpg?1476754974") end end context "for an image where an original does not exist" do - setup do - @asset = "https://cdna.artstation.com/p/assets/images/images/004/730/278/large/mendel-oh-dragonll.jpg" - @download = Downloads::File.new(@asset) - end - should "not try to download the original" do - file, strategy = @download.download! - assert_equal(483_192, ::File.size(file.path)) + assert_downloaded(452_795, "https://cdna.artstation.com/p/assets/images/images/004/730/278/large/mendel-oh-dragonll.jpg") end end context "a download for an ArtStation image hosted on CloudFlare" do - setup do - @asset = "https://cdnb.artstation.com/p/assets/images/images/003/716/071/large/aoi-ogata-hate-city.jpg?1476754974" - end - should "return the original file, not the polished file" do - assert_downloaded(1_880_910, @asset) - end - - should "return the original filesize, not the polished filesize" do - assert_equal(1_880_910, Downloads::File.new(@asset).size) + @asset = "https://cdnb.artstation.com/p/assets/images/images/003/716/071/large/aoi-ogata-hate-city.jpg?1476754974" + assert_downloaded(1_816_438, @asset) end end context "a download for a https://$artist.artstation.com/projects/$id page" do - setup do - @source = "https://dantewontdie.artstation.com/projects/YZK5q" - @download = Downloads::File.new(@source) - end - should "download the original image instead" do - file, strategy = @download.download! - - assert_equal(247_350, ::File.size(file.path)) + assert_downloaded(210_709, "https://dantewontdie.artstation.com/projects/YZK5q") end end end diff --git a/test/unit/downloads/file_test.rb b/test/unit/downloads/file_test.rb deleted file mode 100644 index 7e37787dd..000000000 --- a/test/unit/downloads/file_test.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'test_helper' - -module Downloads - class FileTest < ActiveSupport::TestCase - context "A post download" do - setup do - @source = "http://www.google.com/intl/en_ALL/images/logo.gif" - @download = Downloads::File.new(@source) - end - - context "for a banned IP" do - setup do - Resolv.expects(:getaddress).returns("127.0.0.1").at_least_once - end - - should "not try to download the file" do - assert_raise(Downloads::File::Error) { Downloads::File.new("http://evil.com").download! } - end - - should "not try to fetch the size" do - assert_raise(Downloads::File::Error) { Downloads::File.new("http://evil.com").size } - end - - should "not follow redirects to banned IPs" do - url = "http://httpbin.org/redirect-to?url=http://127.0.0.1" - stub_request(:get, url).to_return(status: 301, headers: { "Location": "http://127.0.0.1" }) - - assert_raise(Downloads::File::Error) { Downloads::File.new(url).download! } - end - - should "not follow redirects that resolve to a banned IP" do - url = "http://httpbin.org/redirect-to?url=http://127.0.0.1.nip.io" - stub_request(:get, url).to_return(status: 301, headers: { "Location": "http://127.0.0.1.xip.io" }) - - assert_raise(Downloads::File::Error) { Downloads::File.new(url).download! } - end - end - - context "that fails" do - should "retry three times before giving up" do - HTTParty.expects(:get).times(3).raises(Errno::ETIMEDOUT) - assert_raises(Errno::ETIMEDOUT) { @download.download! } - end - - should "return an uncorrupted file on the second try" do - bomb = stub("bomb") - bomb.stubs(:code).raises(IOError) - resp = stub("resp", success?: true) - - chunk = stub("a") - chunk.stubs(:code).returns(200) - chunk.stubs(:size).returns(1) - chunk.stubs(:to_s).returns("a") - - HTTParty.expects(:get).twice.multiple_yields(chunk, bomb).then.multiple_yields(chunk, chunk).returns(resp) - @download.stubs(:is_cloudflare?).returns(false) - tempfile, _strategy = @download.download! - - assert_equal("aa", tempfile.read) - end - end - - should "throw an exception when the file is larger than the maximum" do - assert_raise(Downloads::File::Error) do - @download.download!(max_size: 1) - end - end - - should "store the file in the tempfile path" do - tempfile, strategy = @download.download! - assert_operator(tempfile.size, :>, 0, "should have data") - end - - should "correctly save the file when following 302 redirects" do - download = Downloads::File.new("https://yande.re/post/show/578014") - file, strategy = download.download!(url: download.preview_url) - assert_equal(19134, file.size) - end - end - end -end diff --git a/test/unit/downloads/nico_seiga_test.rb b/test/unit/downloads/nico_seiga_test.rb deleted file mode 100644 index de00ed7bb..000000000 --- a/test/unit/downloads/nico_seiga_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "test_helper" - -module Downloads - class NicoSeigaTest < ActiveSupport::TestCase - context "downloading a 'http://seiga.nicovideo.jp/seiga/:id' url" do - should "download the original file" do - @source = "http://seiga.nicovideo.jp/seiga/im4937663" - @rewrite = %r!http://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663! - assert_rewritten(@rewrite, @source) - assert_downloaded(2032, @source) - end - end - - context "downloading a 'http://lohas.nicoseiga.jp/o/:hash/:id' url" do - should "download the original file" do - @source = "http://lohas.nicoseiga.jp/o/910aecf08e542285862954017f8a33a8c32a8aec/1433298801/4937663" - @rewrite = %r!http://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663! - assert_rewritten(@rewrite, @source) - assert_downloaded(2032, @source) - end - end - - context "downloading a 'https://lohas.nicoseiga.jp/thumb/:id' url" do - should "download the original file" do - @source = "https://lohas.nicoseiga.jp/thumb/4937663i" - @rewrite = %r!http://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663! - assert_rewritten(@rewrite, @source) - assert_downloaded(2032, @source) - end - end - end -end diff --git a/test/unit/downloads/pixiv_test.rb b/test/unit/downloads/pixiv_test.rb index a22f8a700..02d923c02 100644 --- a/test/unit/downloads/pixiv_test.rb +++ b/test/unit/downloads/pixiv_test.rb @@ -122,32 +122,17 @@ module Downloads assert_downloaded(@file_size, @file_url, @ref) end end - - context "downloading a pixiv fanbox image" do - should_eventually "work" do - @source = "https://www.pixiv.net/fanbox/creator/12491073/post/82406" - @file_url = "https://fanbox.pixiv.net/images/post/82406/D833IKA7FIesJXL8xx39rrG0.jpeg" - @file_size = 873_387 - - assert_not_rewritten(@file_url, @source) - assert_downloaded(@file_size, @file_url, @source) - end - end end context "An ugoira site for pixiv" do - setup do - @download = Downloads::File.new("http://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") - @tempfile, strategy = @download.download! - @tempfile.close! - end - should "capture the data" do - assert_equal(2, @download.data[:ugoira_frame_data].size) - if @download.data[:ugoira_frame_data][0]["file"] + @strategy = Sources::Strategies.find("http://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") + + assert_equal(2, @strategy.data[:ugoira_frame_data].size) + if @strategy.data[:ugoira_frame_data][0]["file"] assert_equal([{"file" => "000000.jpg", "delay" => 125}, {"file" => "000001.jpg", "delay" => 125}], @download.data[:ugoira_frame_data]) else - assert_equal([{"delay_msec" => 125}, {"delay_msec" => 125}], @download.data[:ugoira_frame_data]) + assert_equal([{"delay_msec" => 125}, {"delay_msec" => 125}], @strategy.data[:ugoira_frame_data]) end end end diff --git a/test/unit/media_file_test.rb b/test/unit/media_file_test.rb index a7516198d..4c77cf5b3 100644 --- a/test/unit/media_file_test.rb +++ b/test/unit/media_file_test.rb @@ -98,6 +98,10 @@ class MediaFileTest < ActiveSupport::TestCase should "determine the correct extension for a flash file" do assert_equal(:swf, MediaFile.open("test/files/compressed.swf").file_ext) end + + should "not fail for empty files" do + assert_equal(:bin, MediaFile.open("test/files/test-empty.bin").file_ext) + end end should "determine the correct md5 for a jpeg file" do diff --git a/test/unit/post_disapproval_test.rb b/test/unit/post_disapproval_test.rb index f16203e48..4e16f401e 100644 --- a/test/unit/post_disapproval_test.rb +++ b/test/unit/post_disapproval_test.rb @@ -67,33 +67,6 @@ class PostDisapprovalTest < ActiveSupport::TestCase end end - context "when sending dmails" do - setup do - @uploaders = FactoryBot.create_list(:user, 2, created_at: 2.weeks.ago) - @disapprovers = FactoryBot.create_list(:mod_user, 2) - - # 2 uploaders, with 2 uploads each, and 2 disapprovals on each upload. - @uploaders.each do |uploader| - FactoryBot.create_list(:post, 2, is_pending: true, uploader: uploader).each do |post| - FactoryBot.create(:post_disapproval, post: post, user: @disapprovers[0]) - FactoryBot.create(:post_disapproval, post: post, user: @disapprovers[1]) - end - end - end - - should "dmail the uploaders" do - bot = FactoryBot.create(:user) - User.stubs(:system).returns(bot) - - assert_difference(["@uploaders[0].dmails.count", "@uploaders[1].dmails.count"], 1) do - PostDisapproval.dmail_messages! - end - - assert(@uploaders[0].dmails.exists?(from: bot, to: @uploaders[0])) - assert(@uploaders[1].dmails.exists?(from: bot, to: @uploaders[1])) - end - end - context "#search" do should "work" do disapproval1 = FactoryBot.create(:post_disapproval, user: @alice, post: @post_1, reason: "breaks_rules") diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index d74b08e8e..54b8ca27f 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -277,6 +277,19 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([child, parent], "-child:garbage") end + should "return posts when using the status of the parent/child" do + parent_of_deleted = create(:post) + deleted = create(:post, is_deleted: true, tag_string: "parent:#{parent_of_deleted.id}") + child_of_deleted = create(:post, tag_string: "parent:#{deleted.id}") + all = [child_of_deleted, deleted, parent_of_deleted] + + assert_tag_match([child_of_deleted], "parent:deleted") + assert_tag_match(all - [child_of_deleted], "-parent:deleted") + + assert_tag_match([parent_of_deleted], "child:deleted") + assert_tag_match(all - [parent_of_deleted], "-child:deleted") + end + should "return posts for the favgroup: metatag" do post1 = create(:post) post2 = create(:post) @@ -757,8 +770,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase create(:saved_search, query: "aaa", labels: ["zzz"], user: CurrentUser.user) create(:saved_search, query: "bbb", user: CurrentUser.user) - Redis.any_instance.stubs(:exists).with("search:aaa").returns(true) - Redis.any_instance.stubs(:exists).with("search:bbb").returns(true) + Redis.any_instance.stubs(:exists?).with("search:aaa").returns(true) + Redis.any_instance.stubs(:exists?).with("search:bbb").returns(true) Redis.any_instance.stubs(:smembers).with("search:aaa").returns([@post1.id]) Redis.any_instance.stubs(:smembers).with("search:bbb").returns([@post2.id]) diff --git a/test/unit/post_sets/post_test.rb b/test/unit/post_sets/post_test.rb index 146600981..77f88800a 100644 --- a/test/unit/post_sets/post_test.rb +++ b/test/unit/post_sets/post_test.rb @@ -129,6 +129,7 @@ module PostSets context "that has a matching artist" do setup do + Tag.find_by(name: "a").update!(category: Tag.categories.artist) @artist = FactoryBot.create(:artist, :name => "a") end diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index ab160680f..b324a89d0 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -954,6 +954,14 @@ class PostTest < ActiveSupport::TestCase @post.update(tag_string: "aaa -fav:self") assert_equal("", @post.fav_string) end + + should "not fail when the fav: metatag is used twice" do + @post.update(tag_string: "aaa fav:self fav:me") + assert_equal("fav:#{@user.id}", @post.fav_string) + + @post.update(tag_string: "aaa -fav:self -fav:me") + assert_equal("", @post.fav_string) + end end context "for a child" do @@ -1287,18 +1295,18 @@ class PostTest < ActiveSupport::TestCase context "with a .webm file extension" do setup do - FactoryBot.create(:tag_implication, antecedent_name: "webm", consequent_name: "animated") + FactoryBot.create(:tag_implication, antecedent_name: "video", consequent_name: "animated") @post.file_ext = "webm" @post.tag_string = "" @post.save end should "have the appropriate file type tag added automatically" do - assert_match(/webm/, @post.tag_string) + assert_match(/video/, @post.tag_string) end should "apply implications after adding the file type tag" do - assert(@post.has_tag?("animated"), "expected 'webm' to imply 'animated'") + assert(@post.has_tag?("animated"), "expected 'video' to imply 'animated'") end end diff --git a/test/unit/post_view_count_service_test.rb b/test/unit/post_view_count_service_test.rb deleted file mode 100644 index ce9b856ac..000000000 --- a/test/unit/post_view_count_service_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'test_helper' - -class PostViewCountServiceTest < ActiveSupport::TestCase - def setup - super - - CurrentUser.user = FactoryBot.create(:user) - CurrentUser.ip_addr = "127.0.0.1" - - PostViewCountService.stubs(:enabled?).returns(true) - Danbooru.config.stubs(:reportbooru_server).returns("http://localhost:1234") - @post = FactoryBot.create(:post) - end - - def teardown - super - CurrentUser.user = nil - CurrentUser.ip_addr = nil - end - - subject { PostViewCountService.new } - - context "#popular_posts" do - setup do - subject.stubs(:fetch_rank).returns([[@post.id, 1]]) - end - - should "return the posts" do - posts = subject.popular_posts - assert_equal(@post.id, posts[0].id) - end - end - - context "#fetch_rank" do - context "success" do - setup do - @date = "2000-01-01" - @body = "[[1,1.0],[2,2.0]]" - stub_request(:get, "localhost:1234/post_views/rank").with(query: {"date" => @date}).to_return(body: @body) - end - - should "return a list" do - json = subject.fetch_rank(@date) - assert(json.is_a?(Array)) - assert_equal(1, json[0][0]) - assert_equal(2, json[1][0]) - end - end - - context "failure" do - setup do - @date = "2000-01-01" - stub_request(:get, "localhost:1234/post_views/rank").with(query: {"date" => @date}).to_return(body: "", status: 400) - end - - should "return nil" do - json = subject.fetch_rank(@date) - assert_nil(json) - end - end - end - - context "#fetch_count" do - context "success" do - setup do - @body = "[[1,5],[2,20]]" - stub_request(:get, "localhost:1234/post_views/#{@post.id}").to_return(body: @body) - end - - should "return a list" do - json = subject.fetch_count(@post.id) - assert(json.is_a?(Array)) - assert_equal(1, json[0][0]) - assert_equal(2, json[1][0]) - end - end - - context "failure" do - setup do - stub_request(:get, "localhost:1234/post_views/#{@post.id}").to_return(body: "", status: 400) - end - - should "return nil" do - json = subject.fetch_count(@post.id) - assert_nil(json) - end - end - end -end diff --git a/test/unit/reportbooru_service_test.rb b/test/unit/reportbooru_service_test.rb new file mode 100644 index 000000000..d31e340f6 --- /dev/null +++ b/test/unit/reportbooru_service_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class ReportbooruServiceTest < ActiveSupport::TestCase + def setup + @service = ReportbooruService.new(reportbooru_server: "http://localhost:1234") + @post = create(:post) + @date = Date.parse("2000-01-01") + end + + context "#popular_posts" do + should "return the list of popular posts on success" do + mock_post_view_rankings(@date, [[@post.id, 100]]) + + posts = @service.popular_posts(@date) + assert_equal([@post], posts) + end + + should "return nothing on failure" do + Danbooru::Http.any_instance.expects(:get).with("http://localhost:1234/post_views/rank?date=#{@date}").returns(HTTP::Response.new(status: 500, body: "", version: "1.1")) + Danbooru::Http.any_instance.expects(:get).with("http://localhost:1234/post_views/rank?date=#{@date.yesterday}").returns(HTTP::Response.new(status: 500, body: "", version: "1.1")) + + assert_equal([], @service.popular_posts(@date)) + end + end +end diff --git a/test/unit/sources/moebooru_test.rb b/test/unit/sources/moebooru_test.rb index 423143ae2..868375295 100644 --- a/test/unit/sources/moebooru_test.rb +++ b/test/unit/sources/moebooru_test.rb @@ -14,7 +14,7 @@ module Sources assert_equal(page_url, site.page_url) if page_url.present? assert_equal(tags.sort, site.tags.map(&:first).sort) assert_equal(profile_url.to_s, site.profile_url.to_s) - assert_equal(size, site.size) + assert_equal(size, site.remote_size) assert_nothing_raised { site.to_h } end @@ -103,7 +103,7 @@ module Sources girls_frontline hara_shoutarou hoodie long_hair pantyhose scar skirt twintails ump-45_(girls_frontline) ump-9_(girls_frontline) ] - @profile_url = "https://www.pixiv.net/member.php?id=22528152" + @profile_url = "https://www.pixiv.net/users/22528152" @data = { site_name: "konachan.com", preview_url: @prev, image_url: @full, page_url: @page, size: @size, tags: @tags, profile_url: @profile_url } assert_source_data_equals(@samp, **@data) diff --git a/test/unit/sources/newgrounds_test.rb b/test/unit/sources/newgrounds_test.rb new file mode 100644 index 000000000..f4195de37 --- /dev/null +++ b/test/unit/sources/newgrounds_test.rb @@ -0,0 +1,112 @@ +require 'test_helper' + +module Sources + class NewGroundsTest < ActiveSupport::TestCase + context "The source for a newgrounds picture" do + setup do + @url = "https://www.newgrounds.com/art/view/natthelich/fire-emblem-marth-plus-progress-pic" + @comment = "https://art.ngfiles.com/comments/57000/iu_57615_7115981.jpg" + @image_1 = Sources::Strategies.find(@url) + @image_2 = Sources::Strategies.find("https://art.ngfiles.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic.png?f1569487181") + @image_3 = Sources::Strategies.find(@comment, @url) + end + + should "get the artist name" do + assert_equal("natthelich", @image_1.artist_name) + assert_equal("natthelich", @image_2.artist_name) + assert_equal("natthelich", @image_3.artist_name) + end + + should "get the artist commentary title" do + assert_equal("Fire Emblem - Marth (plus progress pic)", @image_1.artist_commentary_title) + assert_equal("Fire Emblem - Marth (plus progress pic)", @image_2.artist_commentary_title) + assert_equal("Fire Emblem - Marth (plus progress pic)", @image_3.artist_commentary_title) + end + + should "get profile url" do + assert_equal("https://natthelich.newgrounds.com", @image_1.profile_url) + assert_equal("https://natthelich.newgrounds.com", @image_2.profile_url) + assert_equal("https://natthelich.newgrounds.com", @image_3.profile_url) + end + + should "get the image urls" do + assert_match(%r{https?://art\.ngfiles\.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic\.png(?:\?\w+)?}i, @image_1.image_url) + assert_includes(@image_1.image_urls, @comment) + + assert_match(%r{https?://art\.ngfiles\.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic\.png(?:\?\w+)?}i, @image_2.image_url) + assert_equal(@comment, @image_3.image_url) + end + + should "get the canonical url" do + assert_equal(@url, @image_1.canonical_url) + assert_equal(@url, @image_2.canonical_url) + assert_equal(@url, @image_3.canonical_url) + end + + should "download an image" do + assert_downloaded(630365, @image_1.image_url) + assert_downloaded(630365, @image_2.image_url) + assert_downloaded(129033, @image_3.image_url) + end + + should "get the tags" do + tags = [ + %w[fire-emblem https://www.newgrounds.com/search/conduct/art?match=tags&tags=fire-emblem], + %w[marth https://www.newgrounds.com/search/conduct/art?match=tags&tags=marth ] + ] + assert_equal(tags, @image_1.tags) + assert_equal(tags, @image_2.tags) + assert_equal(tags, @image_3.tags) + end + + should "find the right artist" do + artist_1 = create(:artist, name: "natthelich1", url_string: "https://natthelich.newgrounds.com/art") + artist_2 = create(:artist, name: "natthelich2", url_string: "https://www.newgrounds.com/art/view/natthelich/fire-emblem-marth-plus-progress-pic") + artist_3 = create(:artist, name: "bad_artist", url_string: "https://www.newgrounds.com/art") + + assert_equal([artist_1, artist_2], @image_1.artists) + assert_equal([artist_1, artist_2], @image_2.artists) + assert_equal([artist_1, artist_2], @image_3.artists) + + assert_not_equal([artist_3], @image_1.artists) + end + end + + context "A deleted or not existing picture" do + setup do + @fake_1 = Sources::Strategies.find("https://www.newgrounds.com/art/view/ThisUser/DoesNotExist") + @artist_1 = create(:artist, name: "thisuser", url_string: "https://thisuser.newgrounds.com") + + @fake_2 = Sources::Strategies.find("https://www.newgrounds.com/art/view/natthelich/nopicture") + @artist_2 = create(:artist, name: "natthelich", url_string: "https://natthelich.newgrounds.com") + + @fake_3 = Sources::Strategies.find("https://www.newgrounds.com/art/view/theolebrave/sensitive-pochaco") + @artist_3 = create(:artist, name: "taffytoad", url_string: "https://taffytoad.newgrounds.com") + end + + should "still find the artist name" do + assert_equal("thisuser", @fake_1.artist_name) + assert_equal([@artist_1], @fake_1.artists) + assert_equal("https://thisuser.newgrounds.com", @fake_1.profile_url) + + assert_equal("natthelich", @fake_2.artist_name) + assert_equal([@artist_2], @fake_2.artists) + + assert_equal([@artist_3], @fake_3.artists) + end + end + + context "normalizing for source" do + should "normalize correctly" do + source = "https://art.ngfiles.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic.png?f1569487181" + + assert_equal("https://www.newgrounds.com/art/view/natthelich/fire-emblem-marth-plus-progress-pic", Sources::Strategies.normalize_source(source)) + end + + should "avoid normalizing unnormalizable urls" do + bad_source = "https://art.ngfiles.com/comments/57000/iu_57615_7115981.jpg" + assert_equal(bad_source, Sources::Strategies.normalize_source(bad_source)) + end + end + end +end diff --git a/test/unit/sources/nico_seiga_manga_test.rb b/test/unit/sources/nico_seiga_manga_test.rb deleted file mode 100644 index fe5bdc3b8..000000000 --- a/test/unit/sources/nico_seiga_manga_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'test_helper' - -module Sources - class NicoSeigaTest < ActiveSupport::TestCase - context "The source site for nico seiga" do - setup do - @site = Sources::Strategies.find("http://seiga.nicovideo.jp/watch/mg316708", "http://seiga.nicovideo.jp/watch/mg316708") - end - - should "find the image urls" do - assert_equal(["https://seiga.nicovideo.jp/image/source/8100968", "https://seiga.nicovideo.jp/image/source/8100969", "https://seiga.nicovideo.jp/image/source/8100970", "https://seiga.nicovideo.jp/image/source/8100971", "https://seiga.nicovideo.jp/image/source/8100972", "https://seiga.nicovideo.jp/image/source/8100973", "https://seiga.nicovideo.jp/image/source/8100974", "https://seiga.nicovideo.jp/image/source/8100975"], @site.image_urls) - end - - should "find the page url" do - assert_equal("http://seiga.nicovideo.jp/watch/mg316708", @site.page_url) - end - - should "find the artist name" do - assert_not_nil(@site.artist_name) - end - end - end -end diff --git a/test/unit/sources/nico_seiga_test.rb b/test/unit/sources/nico_seiga_test.rb index 328706281..ff2645d9a 100644 --- a/test/unit/sources/nico_seiga_test.rb +++ b/test/unit/sources/nico_seiga_test.rb @@ -6,32 +6,52 @@ module Sources setup do @site_1 = Sources::Strategies.find("http://lohas.nicoseiga.jp/o/910aecf08e542285862954017f8a33a8c32a8aec/1433298801/4937663") @site_2 = Sources::Strategies.find("http://seiga.nicovideo.jp/seiga/im4937663") - @site_3 = Sources::Strategies.find("http://seiga.nicovideo.jp/watch/mg376206") + @site_3 = Sources::Strategies.find("https://seiga.nicovideo.jp/watch/mg470189?track=ct_episode") end should "get the profile" do assert_equal("http://seiga.nicovideo.jp/user/illust/7017777", @site_1.profile_url) assert_equal("http://seiga.nicovideo.jp/user/illust/7017777", @site_2.profile_url) + assert_equal("http://seiga.nicovideo.jp/user/illust/20797022", @site_3.profile_url) end should "get the artist name" do assert_equal("osamari", @site_1.artist_name) assert_equal("osamari", @site_2.artist_name) + assert_equal("風呂", @site_3.artist_name) end should "get the artist commentary" do assert_equal("コジコジ", @site_2.artist_commentary_title) assert_equal("コジコジのドット絵\nこんなかわいらしい容姿で毒を吐くコジコジが堪らん(切実)", @site_2.artist_commentary_desc) + + assert_equal("ハコ女子 1ハコ目", @site_3.artist_commentary_title) + assert_equal("同じクラスの箱田さんはいつもハコを被っている。しかしてその素顔は…? twitter(@hakojoshi1)にてだいたい毎日更新中。こっちだともうちょっと先まで読めるよ。", @site_3.artist_commentary_desc) end - should "get the image url" do - assert_match(/^http:\/\/lohas\.nicoseiga\.jp\/priv\//, @site_1.image_url) - assert_match(/^http:\/\/lohas\.nicoseiga\.jp\/priv\//, @site_2.image_url) + should "get the image url(s)" do + assert_match(%r{^https?://lohas\.nicoseiga\.jp/priv/}, @site_1.image_url) + assert_match(%r{^https?://lohas\.nicoseiga\.jp/priv/}, @site_2.image_url) + + expected = %w[ + https://seiga.nicovideo.jp/image/source/10315315 + https://seiga.nicovideo.jp/image/source/10315318 + https://seiga.nicovideo.jp/image/source/10315319 + https://seiga.nicovideo.jp/image/source/10315320 + https://seiga.nicovideo.jp/image/source/10315321 + https://seiga.nicovideo.jp/image/source/10315322 + https://seiga.nicovideo.jp/image/source/10315323 + https://seiga.nicovideo.jp/image/source/10315324 + https://seiga.nicovideo.jp/image/source/10315316 + ] + assert_equal(expected.sort, @site_3.image_urls.sort) + assert_match(%r{^https?://lohas\.nicoseiga\.jp/priv/}, @site_3.image_url) end should "get the canonical url" do - assert_match(%r!\Ahttps?://lohas\.nicoseiga\.jp/priv/\h{40}/\d+/4937663!, @site_1.canonical_url) - assert_match(%r!\Ahttps?://lohas\.nicoseiga\.jp/priv/\h{40}/\d+/4937663!, @site_2.canonical_url) + assert_equal("https://seiga.nicovideo.jp/image/source/4937663", @site_1.canonical_url) + assert_equal("https://seiga.nicovideo.jp/seiga/im4937663", @site_2.canonical_url) + assert_equal("https://seiga.nicovideo.jp/watch/mg470189", @site_3.canonical_url) end should "get the tags" do @@ -42,23 +62,70 @@ module Sources assert_not(@site_2.tags.empty?) first_tag = @site_2.tags.first assert_equal(["アニメ", "https://seiga.nicovideo.jp/tag/%E3%82%A2%E3%83%8B%E3%83%A1"], first_tag) + + assert_not(@site_3.tags.empty?) + first_tag = @site_3.tags.first + assert_equal(["4コマ漫画", "https://seiga.nicovideo.jp/manga/tag/4%E3%82%B3%E3%83%9E%E6%BC%AB%E7%94%BB"], first_tag) end should "convert a page into a json representation" do - assert_nothing_raised do - @site_1.to_h - end - assert_nothing_raised do - @site_2.to_h - end + assert_nothing_raised { @site_1.to_h } + assert_nothing_raised { @site_2.to_h } + assert_nothing_raised { @site_3.to_h } end should "work for a https://lohas.nicoseiga.jp/thumb/${id}i url" do site = Sources::Strategies.find("https://lohas.nicoseiga.jp/thumb/6844226i") - full_image_url = %r!https?://lohas.nicoseiga.jp/priv/[a-f0-9]{40}/[0-9]+/6844226! - assert_match(full_image_url, site.image_url) - assert_match(full_image_url, site.canonical_url) + assert_match(%r!https?://lohas.nicoseiga.jp/priv/[a-f0-9]{40}/[0-9]+/6844226!, site.image_url) + assert_match("https://seiga.nicovideo.jp/seiga/im6844226", site.canonical_url) + end + end + + context "A manga upload through bookmarklet" do + setup do + @url = "https://seiga.nicovideo.jp/image/source/9146749" + @ref = "https://seiga.nicovideo.jp/watch/mg389884" + @site = Sources::Strategies.find(@url, @ref) + end + + should "get the correct pic" do + assert_match(%r!https?://lohas.nicoseiga.jp/priv/[a-f0-9]{40}/[0-9]+/9146749!, @site.image_url) + end + + should "set the correct source" do + assert_equal(@ref, @site.canonical_url) + end + end + + context "A nicoseiga video" do + should "not raise anything" do + site = Sources::Strategies.find("https://www.nicovideo.jp/watch/sm36465441") + assert_nothing_raised { site.to_h } + end + end + + context "An anonymous picture" do + should "still work" do + site = Sources::Strategies.find("https://seiga.nicovideo.jp/seiga/im520647") + + assert_nothing_raised { site.to_h } + end + end + + context "An age-restricted picture" do + should "still work" do + site = Sources::Strategies.find("http://seiga.nicovideo.jp/seiga/im9208126") + + assert_match(%r!https?://lohas.nicoseiga.jp/priv/[a-f0-9]{40}/[0-9]+/9208126!, site.image_url) + assert_nothing_raised { site.to_h } + end + end + + context "An oekaki picture" do + should "still work" do + site = Sources::Strategies.find("https://dic.nicovideo.jp/oekaki/52833.png") + assert_nothing_raised { site.to_h } end end @@ -69,10 +136,10 @@ module Sources source3 = "http://lohas.nicoseiga.jp/o/910aecf08e542285862954017f8a33a8c32a8aec/1433298801/4937663" source4 = "http://seiga.nicovideo.jp/image/source?id=3312222" - assert_equal("https://seiga.nicovideo.jp/seiga/im3521156", Sources::Strategies.normalize_source(source1)) - assert_equal("https://seiga.nicovideo.jp/seiga/im3583893", Sources::Strategies.normalize_source(source2)) - assert_equal("https://seiga.nicovideo.jp/seiga/im4937663", Sources::Strategies.normalize_source(source3)) - assert_equal("https://seiga.nicovideo.jp/seiga/im3312222", Sources::Strategies.normalize_source(source4)) + assert_equal("https://seiga.nicovideo.jp/image/source/3521156", Sources::Strategies.normalize_source(source1)) + assert_equal("https://seiga.nicovideo.jp/image/source/3583893", Sources::Strategies.normalize_source(source2)) + assert_equal("https://seiga.nicovideo.jp/image/source/4937663", Sources::Strategies.normalize_source(source3)) + assert_equal("https://seiga.nicovideo.jp/image/source/3312222", Sources::Strategies.normalize_source(source4)) end should "avoid normalizing unnormalizable urls" do @@ -80,5 +147,32 @@ module Sources assert_equal(bad_source, Sources::Strategies.normalize_source(bad_source)) end end + + context "downloading a 'http://seiga.nicovideo.jp/seiga/:id' url" do + should "download the original file" do + @source = "http://seiga.nicovideo.jp/seiga/im4937663" + @rewrite = %r{https://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663} + assert_rewritten(@rewrite, @source) + assert_downloaded(2_032, @source) + end + end + + context "downloading a 'http://lohas.nicoseiga.jp/o/:hash/:id' url" do + should "download the original file" do + @source = "http://lohas.nicoseiga.jp/o/910aecf08e542285862954017f8a33a8c32a8aec/1433298801/4937663" + @rewrite = %r{https://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663} + assert_rewritten(@rewrite, @source) + assert_downloaded(2_032, @source) + end + end + + context "downloading a 'https://lohas.nicoseiga.jp/thumb/:id' url" do + should "download the original file" do + @source = "https://lohas.nicoseiga.jp/thumb/4937663i" + @rewrite = %r{https://lohas.nicoseiga.jp/priv/\h{40}/\d+/4937663} + assert_rewritten(@rewrite, @source) + assert_downloaded(2_032, @source) + end + end end end diff --git a/test/unit/sources/nijie_test.rb b/test/unit/sources/nijie_test.rb index 796bcfd46..c4f7b88c8 100644 --- a/test/unit/sources/nijie_test.rb +++ b/test/unit/sources/nijie_test.rb @@ -43,7 +43,7 @@ module Sources should "get the image url" do assert_equal("https://pic.nijie.net/03/nijie_picture/728995_20170505014820_0.jpg", @site.image_url) - assert_http_size(132_555, @site.image_url) + assert_downloaded(132_555, @site.image_url) end should "get the canonical url" do @@ -53,7 +53,7 @@ module Sources should "get the preview url" do assert_equal("https://pic.nijie.net/03/__rs_l170x170/nijie_picture/728995_20170505014820_0.jpg", @site.preview_url) assert_equal([@site.preview_url], @site.preview_urls) - assert_http_exists(@site.preview_url) + assert_downloaded(132_555, @site.preview_url) end should "get the profile" do @@ -187,8 +187,6 @@ module Sources desc = <<-EOS.strip_heredoc.chomp foo [b]bold[/b] [i]italics[/i] [s]strike[/s] red - - EOS @@ -207,8 +205,8 @@ module Sources assert_equal("https://nijie.info/members.php?id=236014", site.profile_url) assert_nothing_raised { site.to_h } - assert_http_size(3619, site.image_url) - assert_http_exists(site.preview_url) + assert_downloaded(3619, site.image_url) + assert_downloaded(3619, site.preview_url) end end diff --git a/test/unit/sources/pixiv_test.rb b/test/unit/sources/pixiv_test.rb index f4778615e..e58cd029c 100644 --- a/test/unit/sources/pixiv_test.rb +++ b/test/unit/sources/pixiv_test.rb @@ -15,10 +15,7 @@ module Sources def get_source(source) @site = Sources::Strategies.find(source) - @site - rescue Net::OpenTimeout - skip "Remote connection to #{source} failed" end context "in all cases" do @@ -73,17 +70,6 @@ module Sources end end - context "A https://www.pixiv.net/fanbox/creator/*/post/* source" do - should_eventually "work" do - @site = Sources::Strategies.find("http://www.pixiv.net/fanbox/creator/554149/post/82555") - - assert_equal("TYONE(お仕事募集中)", @site.artist_name) - assert_equal("https://www.pixiv.net/member.php?id=554149", @site.profile_url) - assert_equal("https://fanbox.pixiv.net/images/post/82555/Lyyeb6dDLcQZmy09nqLZapuS.jpeg", @site.image_url) - assert_nothing_raised { @site.to_h } - end - end - context "A https://www.pixiv.net/*/artworks/* source" do should "work" do @site = Sources::Strategies.find("https://www.pixiv.net/en/artworks/64476642") @@ -104,13 +90,17 @@ module Sources end should "get the profile" do - assert_equal("https://www.pixiv.net/member.php?id=696859", @site.profile_url) + assert_equal("https://www.pixiv.net/users/696859", @site.profile_url) end should "get the artist name" do assert_equal("uroobnad", @site.artist_name) end + should "get the remote image size" do + assert_equal(854_653, @site.remote_size) + end + should "get the full size image url" do assert_equal("https://i.pximg.net/img-original/img/2017/11/21/05/12/37/65981735_p0.jpg", @site.image_url) end @@ -205,7 +195,7 @@ module Sources should "convert illust links and member links to dtext" do get_source("https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63421642") - dtext_desc = %(foo 【pixiv #46337015 "»":[/posts?tags=pixiv:46337015]】bar 【pixiv #14901720 "»":[/posts?tags=pixiv:14901720]】\n\nbaz【"user/83739":[https://www.pixiv.net/member.php?id=83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fmember.php%3Fid%3D83739]】) + dtext_desc = %(foo 【pixiv #46337015 "»":[/posts?tags=pixiv:46337015]】bar 【pixiv #14901720 "»":[/posts?tags=pixiv:14901720]】\n\nbaz【"user/83739":[https://www.pixiv.net/users/83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F83739]】) assert_equal(dtext_desc, @site.dtext_artist_commentary_desc) end end @@ -297,24 +287,18 @@ module Sources assert_equal("uroobnad", source.tag_name) assert_equal(["uroobnad"], source.other_names) - assert_includes(source.profile_urls, "https://www.pixiv.net/member.php?id=696859") + assert_includes(source.profile_urls, "https://www.pixiv.net/users/696859") assert_includes(source.profile_urls, "https://www.pixiv.net/stacc/uroobnad") end end context "parsing illust ids" do should "parse ids from illust urls" do - assert_illust_id(46324488, "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=46324488") - assert_illust_id(46324488, "https://www.pixiv.net/member_illust.php?mode=manga_big&illust_id=46324488&page=0") - assert_illust_id(46324488, "https://i.pximg.net/img-original/img/2014/10/03/18/10/20/46324488_p0.png") - assert_illust_id(46324488, "https://i.pximg.net/img-master/img/2014/10/03/18/10/20/46324488_p0_master1200.jpg") - assert_illust_id(65015428, "https://tc-pximg01.techorus-cdn.com/img-original/img/2017/09/18/03/18/24/65015428_p4.png") assert_illust_id(46785915, "https://i.pximg.net/c/250x250_80_a2/img-master/img/2014/10/29/09/27/19/46785915_p0_square1200.jpg") assert_illust_id(79584713, "https://i-f.pximg.net/img-original/img/2020/02/19/00/40/18/79584713_p0.png") - assert_illust_id(46323924, "http://i1.pixiv.net/img-zip-ugoira/img/2014/10/03/17/29/16/46323924_ugoira1920x1080.zip") assert_illust_id(46304396, "http://i1.pixiv.net/img-original/img/2014/10/02/13/51/23/46304396_p0.png") assert_illust_id(46304396, "http://i1.pixiv.net/c/600x600/img-master/img/2014/10/02/13/51/23/46304396_p0_master1200.jpg") @@ -333,6 +317,15 @@ module Sources assert_illust_id(18557054, "http://www.pixiv.net/artworks/18557054") end + should "parse ids from expicit/guro illust urls" do + assert_illust_id(46324488, "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=46324488") + assert_illust_id(46324488, "https://www.pixiv.net/member_illust.php?mode=manga_big&illust_id=46324488&page=0") + assert_illust_id(46324488, "https://i.pximg.net/img-original/img/2014/10/03/18/10/20/46324488_p0.png") + assert_illust_id(46324488, "https://i.pximg.net/img-master/img/2014/10/03/18/10/20/46324488_p0_master1200.jpg") + + assert_illust_id(46323924, "http://i1.pixiv.net/img-zip-ugoira/img/2014/10/03/17/29/16/46323924_ugoira1920x1080.zip") + end + should "not misparse ids from fanbox urls" do assert_nil_illust_id("https://fanbox.pixiv.net/images/post/39714/JvjJal8v1yLgc5DPyEI05YpT.png") assert_nil_illust_id("https://pixiv.pximg.net/fanbox/public/images/creator/1566167/profile/Ix6bnJmTaOAFZhXHLbWyIY1e.jpeg") diff --git a/test/unit/sources/tumblr_test.rb b/test/unit/sources/tumblr_test.rb index 3b774f934..fdba14560 100644 --- a/test/unit/sources/tumblr_test.rb +++ b/test/unit/sources/tumblr_test.rb @@ -169,7 +169,7 @@ module Sources context "The source for a 'http://ve.media.tumblr.com/*' video post with inline images" do setup do - @url = "https://ve.media.tumblr.com/tumblr_os31dkexhK1wsfqep.mp4" + @url = "https://va.media.tumblr.com/tumblr_os31dkexhK1wsfqep.mp4" @ref = "https://noizave.tumblr.com/post/162222617101" end @@ -177,7 +177,7 @@ module Sources should "get the video and inline images" do site = Sources::Strategies.find(@url, @ref) urls = %w[ - https://ve.media.tumblr.com/tumblr_os31dkexhK1wsfqep.mp4 + https://va.media.tumblr.com/tumblr_os31dkexhK1wsfqep.mp4 https://media.tumblr.com/afed9f5b3c33c39dc8c967e262955de2/tumblr_inline_os31dclyCR1v11u29_1280.png ] diff --git a/test/unit/sources/twitter_test.rb b/test/unit/sources/twitter_test.rb index a3b9728f8..96e187ae2 100644 --- a/test/unit/sources/twitter_test.rb +++ b/test/unit/sources/twitter_test.rb @@ -244,6 +244,14 @@ module Sources end end + context "A profile banner image" do + should "work" do + @site = Sources::Strategies.find("https://pbs.twimg.com/profile_banners/1225702850002468864/1588597370/1500x500") + assert_equal(@site.image_url, @site.url) + assert_nothing_raised { @site.to_h } + end + end + context "A tweet containing non-normalized Unicode text" do should "be normalized to nfkc" do site = Sources::Strategies.find("https://twitter.com/aprilarcus/status/367557195186970624") diff --git a/test/unit/storage_manager_test.rb b/test/unit/storage_manager_test.rb index 9759a2918..9d39b03e5 100644 --- a/test/unit/storage_manager_test.rb +++ b/test/unit/storage_manager_test.rb @@ -1,8 +1,6 @@ require 'test_helper' class StorageManagerTest < ActiveSupport::TestCase - BASE_DIR = "#{Rails.root}/tmp/test-storage" - setup do CurrentUser.ip_addr = "127.0.0.1" end @@ -45,25 +43,21 @@ class StorageManagerTest < ActiveSupport::TestCase context "StorageManager::Local" do setup do - @storage_manager = StorageManager::Local.new(base_dir: BASE_DIR, base_url: "/data") - end - - teardown do - FileUtils.rm_rf(BASE_DIR) + @storage_manager = StorageManager::Local.new(base_dir: @temp_dir, base_url: "/data") end context "#store method" do should "store the file" do - @storage_manager.store(StringIO.new("data"), "#{BASE_DIR}/test.txt") + @storage_manager.store(StringIO.new("data"), "#{@temp_dir}/test.txt") - assert("data", File.read("#{BASE_DIR}/test.txt")) + assert("data", File.read("#{@temp_dir}/test.txt")) end should "overwrite the file if it already exists" do - @storage_manager.store(StringIO.new("foo"), "#{BASE_DIR}/test.txt") - @storage_manager.store(StringIO.new("bar"), "#{BASE_DIR}/test.txt") + @storage_manager.store(StringIO.new("foo"), "#{@temp_dir}/test.txt") + @storage_manager.store(StringIO.new("bar"), "#{@temp_dir}/test.txt") - assert("bar", File.read("#{BASE_DIR}/test.txt")) + assert("bar", File.read("#{@temp_dir}/test.txt")) end end @@ -72,7 +66,7 @@ class StorageManagerTest < ActiveSupport::TestCase @storage_manager.store(StringIO.new("data"), "test.txt") @storage_manager.delete("test.txt") - assert_not(File.exist?("#{BASE_DIR}/test.txt")) + assert_not(File.exist?("#{@temp_dir}/test.txt")) end should "not fail if the file doesn't exist" do @@ -88,9 +82,9 @@ class StorageManagerTest < ActiveSupport::TestCase @storage_manager.store_file(StringIO.new("data"), @post, :large) @storage_manager.store_file(StringIO.new("data"), @post, :original) - @file_path = "#{BASE_DIR}/preview/#{@post.md5}.jpg" - @large_file_path = "#{BASE_DIR}/sample/sample-#{@post.md5}.jpg" - @preview_file_path = "#{BASE_DIR}/#{@post.md5}.#{@post.file_ext}" + @file_path = "#{@temp_dir}/preview/#{@post.md5}.jpg" + @large_file_path = "#{@temp_dir}/sample/sample-#{@post.md5}.jpg" + @preview_file_path = "#{@temp_dir}/#{@post.md5}.#{@post.file_ext}" end should "store the files at the correct path" do @@ -134,12 +128,12 @@ class StorageManagerTest < ActiveSupport::TestCase context "when the original_subdir option is used" do should "store original files at the correct path" do @post = FactoryBot.create(:post, file_ext: "png") - @storage_manager = StorageManager::Local.new(base_dir: BASE_DIR, base_url: "/data", original_subdir: "original/") + @storage_manager = StorageManager::Local.new(base_dir: @temp_dir, base_url: "/data", original_subdir: "original/") - assert_equal("#{BASE_DIR}/original/#{@post.md5}.png", @storage_manager.file_path(@post, @post.file_ext, :original)) + assert_equal("#{@temp_dir}/original/#{@post.md5}.png", @storage_manager.file_path(@post, @post.file_ext, :original)) @storage_manager.store_file(StringIO.new("data"), @post, :original) - assert_equal(true, File.exist?("#{BASE_DIR}/original/#{@post.md5}.png")) + assert_equal(true, File.exist?("#{@temp_dir}/original/#{@post.md5}.png")) end end end @@ -151,24 +145,20 @@ class StorageManagerTest < ActiveSupport::TestCase @storage_manager = StorageManager::Hybrid.new do |id, md5, file_ext, type| if id.odd? - StorageManager::Local.new(base_dir: "#{BASE_DIR}/i1", base_url: "/i1") + StorageManager::Local.new(base_dir: "#{@temp_dir}/i1", base_url: "/i1") else - StorageManager::Local.new(base_dir: "#{BASE_DIR}/i2", base_url: "/i2") + StorageManager::Local.new(base_dir: "#{@temp_dir}/i2", base_url: "/i2") end end end - teardown do - FileUtils.rm_rf(BASE_DIR) - end - context "#store_file method" do should "store odd-numbered posts under /i1 and even-numbered posts under /i2" do @storage_manager.store_file(StringIO.new("post1"), @post1, :original) @storage_manager.store_file(StringIO.new("post2"), @post2, :original) - assert(File.exist?("#{BASE_DIR}/i1/#{@post1.md5}.png")) - assert(File.exist?("#{BASE_DIR}/i2/#{@post2.md5}.png")) + assert(File.exist?("#{@temp_dir}/i1/#{@post1.md5}.png")) + assert(File.exist?("#{@temp_dir}/i2/#{@post2.md5}.png")) end end diff --git a/test/unit/upload_service_test.rb b/test/unit/upload_service_test.rb index b6b7bbf15..51a7a30b5 100644 --- a/test/unit/upload_service_test.rb +++ b/test/unit/upload_service_test.rb @@ -131,12 +131,9 @@ class UploadServiceTest < ActiveSupport::TestCase end should "download the file" do - begin - @service = UploadService::Preprocessor.new(source: @source, referer_url: @ref) - @upload = @service.start! - rescue Net::OpenTimeout - skip "network failure" - end + @service = UploadService::Preprocessor.new(source: @source, referer_url: @ref) + @upload = @service.start! + assert_equal("preprocessed", @upload.status) assert_equal(294591, @upload.file_size) assert_equal("jpg", @upload.file_ext) @@ -155,11 +152,8 @@ class UploadServiceTest < ActiveSupport::TestCase skip unless MediaFile::Ugoira.videos_enabled? @service = UploadService::Preprocessor.new(source: @source) - begin - @upload = @service.start! - rescue Net::OpenTimeout - skip "network problems" - end + @upload = @service.start! + assert_equal("preprocessed", @upload.status) assert_equal(2804, @upload.file_size) assert_equal("zip", @upload.file_ext) @@ -176,11 +170,8 @@ class UploadServiceTest < ActiveSupport::TestCase should "download the file" do @service = UploadService::Preprocessor.new(source: @source) - begin - @upload = @service.start! - rescue Net::OpenTimeout - skip "network problems" - end + @upload = @service.start! + assert_equal("preprocessed", @upload.status) assert_equal(181309, @upload.file_size) assert_equal("jpg", @upload.file_ext) @@ -212,7 +203,7 @@ class UploadServiceTest < ActiveSupport::TestCase context "on timeout errors" do setup do @source = "https://cdn.donmai.us/original/93/f4/93f4dd66ef1eb11a89e56d31f9adc8d0.jpg" - HTTParty.stubs(:get).raises(Net::ReadTimeout) + Danbooru::Http.any_instance.stubs(:get).raises(HTTP::TimeoutError) end should "leave the upload in an error state" do @@ -512,36 +503,28 @@ class UploadServiceTest < ActiveSupport::TestCase context "a post with a pixiv html source" do should "replace with the full size image" do - begin - as(@user) do - @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") - end - - assert_equal(80, @post.image_width) - assert_equal(82, @post.image_height) - assert_equal(16275, @post.file_size) - assert_equal("png", @post.file_ext) - assert_equal("4ceadc314938bc27f3574053a3e1459a", @post.md5) - assert_equal("4ceadc314938bc27f3574053a3e1459a", Digest::MD5.file(@post.file).hexdigest) - assert_equal("https://i.pximg.net/img-original/img/2017/04/04/08/54/15/62247350_p0.png", @post.replacements.last.replacement_url) - assert_equal("https://i.pximg.net/img-original/img/2017/04/04/08/54/15/62247350_p0.png", @post.source) - rescue Net::OpenTimeout - skip "Remote connection to Pixiv failed" + as(@user) do + @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") end + + assert_equal(80, @post.image_width) + assert_equal(82, @post.image_height) + assert_equal(16275, @post.file_size) + assert_equal("png", @post.file_ext) + assert_equal("4ceadc314938bc27f3574053a3e1459a", @post.md5) + assert_equal("4ceadc314938bc27f3574053a3e1459a", Digest::MD5.file(@post.file).hexdigest) + assert_equal("https://i.pximg.net/img-original/img/2017/04/04/08/54/15/62247350_p0.png", @post.replacements.last.replacement_url) + assert_equal("https://i.pximg.net/img-original/img/2017/04/04/08/54/15/62247350_p0.png", @post.source) end should "delete the old files after thirty days" do - begin - @post.unstub(:queue_delete_files) - FileUtils.expects(:rm_f).times(3) + @post.unstub(:queue_delete_files) + FileUtils.expects(:rm_f).times(3) - as(@user) { @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") } + as(@user) { @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") } - travel_to((PostReplacement::DELETION_GRACE_PERIOD + 1).days.from_now) do - perform_enqueued_jobs - end - rescue Net::OpenTimeout - skip "Remote connection to Pixiv failed" + travel_to((PostReplacement::DELETION_GRACE_PERIOD + 1).days.from_now) do + perform_enqueued_jobs end end end @@ -568,34 +551,30 @@ class UploadServiceTest < ActiveSupport::TestCase context "a post that is replaced to another file then replaced back to the original file" do should "not delete the original files" do - begin - skip unless MediaFile::Ugoira.videos_enabled? - @post.unstub(:queue_delete_files) + skip unless MediaFile::Ugoira.videos_enabled? + @post.unstub(:queue_delete_files) - # this is called thrice to delete the file for 62247364 - FileUtils.expects(:rm_f).times(3) + # this is called thrice to delete the file for 62247364 + FileUtils.expects(:rm_f).times(3) - as(@user) do - @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") - @post.reload - @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") - @post.reload - Upload.destroy_all - @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") - end - - assert_nothing_raised { @post.file(:original) } - assert_nothing_raised { @post.file(:preview) } - - assert_enqueued_jobs 3, only: DeletePostFilesJob - travel PostReplacement::DELETION_GRACE_PERIOD + 1.day - assert_raise(Post::DeletionError) { perform_enqueued_jobs } - - assert_nothing_raised { @post.file(:original) } - assert_nothing_raised { @post.file(:preview) } - rescue Net::OpenTimeout - skip "Remote connection to Pixiv failed" + as(@user) do + @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") + @post.reload + @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") + @post.reload + Upload.destroy_all + @post.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") end + + assert_nothing_raised { @post.file(:original) } + assert_nothing_raised { @post.file(:preview) } + + assert_enqueued_jobs 3, only: DeletePostFilesJob + travel PostReplacement::DELETION_GRACE_PERIOD + 1.day + assert_raise(Post::DeletionError) { perform_enqueued_jobs } + + assert_nothing_raised { @post.file(:original) } + assert_nothing_raised { @post.file(:preview) } end end @@ -609,39 +588,35 @@ class UploadServiceTest < ActiveSupport::TestCase should "not delete the still active files" do # swap the images between @post1 and @post2. - begin - as(@user) do - skip unless MediaFile::Ugoira.videos_enabled? + as(@user) do + skip unless MediaFile::Ugoira.videos_enabled? - @post1.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") - @post2.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") - assert_equal("4ceadc314938bc27f3574053a3e1459a", @post1.md5) - assert_equal("cad1da177ef309bf40a117c17b8eecf5", @post2.md5) + @post1.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") + @post2.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") + assert_equal("4ceadc314938bc27f3574053a3e1459a", @post1.md5) + assert_equal("cad1da177ef309bf40a117c17b8eecf5", @post2.md5) - @post2.reload - @post2.replace!(replacement_url: "https://cdn.donmai.us/original/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg") - assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", @post2.md5) - Upload.destroy_all - @post1.reload - @post2.reload + @post2.reload + @post2.replace!(replacement_url: "https://cdn.donmai.us/original/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg") + assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", @post2.md5) + Upload.destroy_all + @post1.reload + @post2.reload - @post1.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") - @post2.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") - assert_equal("cad1da177ef309bf40a117c17b8eecf5", @post1.md5) - assert_equal("4ceadc314938bc27f3574053a3e1459a", @post2.md5) - end - - travel_to (PostReplacement::DELETION_GRACE_PERIOD + 1).days.from_now do - assert_raise(Post::DeletionError) do - perform_enqueued_jobs - end - end - - assert_nothing_raised { @post1.file(:original) } - assert_nothing_raised { @post2.file(:original) } - rescue Net::OpenTimeout - skip "Remote connection to Pixiv failed" + @post1.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") + @post2.replace!(replacement_url: "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247350") + assert_equal("cad1da177ef309bf40a117c17b8eecf5", @post1.md5) + assert_equal("4ceadc314938bc27f3574053a3e1459a", @post2.md5) end + + travel_to (PostReplacement::DELETION_GRACE_PERIOD + 1).days.from_now do + assert_raise(Post::DeletionError) do + perform_enqueued_jobs + end + end + + assert_nothing_raised { @post1.file(:original) } + assert_nothing_raised { @post2.file(:original) } end end @@ -885,12 +860,8 @@ class UploadServiceTest < ActiveSupport::TestCase end should "record the canonical source" do - begin - post = subject.new({}).create_post_from_upload(@upload) - assert_equal(@source, post.source) - rescue Net::OpenTimeout - skip "network failure" - end + post = subject.new({}).create_post_from_upload(@upload) + assert_equal(@source, post.source) end end @@ -976,8 +947,9 @@ class UploadServiceTest < ActiveSupport::TestCase should "delete stale upload records" do @upload = as(@user) { UploadService.new(file: upload_file("test/files/test.jpg")).start! } + @upload.update!(created_at: 1.month.ago) - assert_difference("Upload.count", -1) { Upload.prune!(0.seconds.ago) } + assert_difference("Upload.count", -1) { Upload.prune! } end should "delete unused files after deleting the upload" do diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 79fac4cda..36d34af45 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -34,7 +34,7 @@ class UserTest < ActiveSupport::TestCase @user.promote_to!(User::Levels::GOLD) end - assert(@user.dmails.exists?(from: bot, to: @user, title: "You have been promoted")) + assert(@user.dmails.exists?(from: bot, to: @user, title: "Your account has been updated")) refute(@user.dmails.exists?(from: bot, to: @user, title: "Your user record has been updated")) end end diff --git a/yarn.lock b/yarn.lock index 66ee35855..3a219010c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,35 +2,35 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff" - integrity sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: - "@babel/highlight" "^7.10.1" + "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.1.tgz#b1085ffe72cd17bf2c0ee790fc09f9626011b2db" - integrity sha512-CHvCj7So7iCkGKPRFUfryXIkU2gSBw7VSZFYLsqVhrS47269VK2Hfi9S/YcublPMW8k1u2bQBlbDruoQEm4fgw== +"@babel/compat-data@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.4.tgz#706a6484ee6f910b719b696a9194f8da7d7ac241" + integrity sha512-t+rjExOrSVvjQQXNp5zAIYDp00KjdvGl/TpDX5REPr0S9IAIPQMTilcfG6q8c0QFmj9lSTVySV2VTsyggvtNIw== dependencies: browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" "@babel/core@>=7.9.0", "@babel/core@^7.9.0": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a" - integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.4.tgz#780e8b83e496152f8dd7df63892b2e052bf1d51d" + integrity sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA== dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/generator" "^7.10.2" - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helpers" "^7.10.1" - "@babel/parser" "^7.10.2" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.2" + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,312 +40,312 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.10.1", "@babel/generator@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9" - integrity sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA== +"@babel/generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.4.tgz#e49eeed9fe114b62fa5b181856a43a5e32f5f243" + integrity sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng== dependencies: - "@babel/types" "^7.10.2" + "@babel/types" "^7.10.4" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268" - integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw== +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.1.tgz#0ec7d9be8174934532661f87783eb18d72290059" - integrity sha512-cQpVq48EkYxUU0xozpGCLla3wlkdRRqLWu1ksFMXA9CM5KQmyyRpSEsYXbao7JUkOw/tAaYKCaYyZq6HOFYtyw== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" + integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== dependencies: - "@babel/helper-explode-assignable-expression" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-explode-assignable-expression" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz#a17d9723b6e2c750299d2a14d4637c76936d8285" - integrity sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA== +"@babel/helper-compilation-targets@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" + integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== dependencies: - "@babel/compat-data" "^7.10.1" + "@babel/compat-data" "^7.10.4" browserslist "^4.12.0" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.1": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz#7474295770f217dbcf288bf7572eb213db46ee67" - integrity sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ== +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.4.tgz#2d4015d0136bd314103a70d84a7183e4b344a355" + integrity sha512-9raUiOsXPxzzLjCXeosApJItoMnX3uyT4QdM2UldffuGApNrF8e938MwNpDCK9CPoyxrEoCgT+hObJc3mZa6lQ== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-member-expression-to-functions" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" -"@babel/helper-create-regexp-features-plugin@^7.10.1", "@babel/helper-create-regexp-features-plugin@^7.8.3": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd" - integrity sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA== +"@babel/helper-create-regexp-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" + integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-regex" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" -"@babel/helper-define-map@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.1.tgz#5e69ee8308648470dd7900d159c044c10285221d" - integrity sha512-+5odWpX+OnvkD0Zmq7panrMuAGQBu6aPUgvMzuMGo4R+jUOvealEj2hiqI6WhxgKrTpFoFj0+VdsuA8KDxHBDg== +"@babel/helper-define-map@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.4.tgz#f037ad794264f729eda1889f4ee210b870999092" + integrity sha512-nIij0oKErfCnLUCWaCaHW0Bmtl2RO9cN7+u2QT8yqTywgALKlyUVOvHDElh+b5DwVC6YB1FOYFOTWcN/+41EDA== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/types" "^7.10.4" lodash "^4.17.13" -"@babel/helper-explode-assignable-expression@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.1.tgz#e9d76305ee1162ca467357ae25df94f179af2b7e" - integrity sha512-vcUJ3cDjLjvkKzt6rHrl767FeE7pMEYfPanq5L16GRtrXIoznc0HykNW2aEYkcnP76P0isoqJ34dDMFZwzEpJg== +"@babel/helper-explode-assignable-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" + integrity sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A== dependencies: - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz#92bd63829bfc9215aca9d9defa85f56b539454f4" - integrity sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ== +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz#7303390a81ba7cb59613895a192b93850e373f7d" - integrity sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw== +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-hoist-variables@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.1.tgz#7e77c82e5dcae1ebf123174c385aaadbf787d077" - integrity sha512-vLm5srkU8rI6X3+aQ1rQJyfjvCBLXP8cAGeuw04zeAM2ItKb1e7pmVmLyHb4sDaAYnLL13RHOZPLEtcGZ5xvjg== +"@babel/helper-hoist-variables@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" + integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz#432967fd7e12a4afef66c4687d4ca22bc0456f15" - integrity sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g== +"@babel/helper-member-expression-to-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz#7cd04b57dfcf82fce9aeae7d4e4452fa31b8c7c4" + integrity sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-module-imports@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.1.tgz#dd331bd45bccc566ce77004e9d05fe17add13876" - integrity sha512-SFxgwYmZ3HZPyZwJRiVNLRHWuW2OgE5k2nrVs6D9Iv4PPnXVffuEHy83Sfx/l4SqF+5kyJXjAyUmrG7tNm+qVg== +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622" - integrity sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg== +"@babel/helper-module-transforms@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d" + integrity sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-simple-access" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz#b4a1f2561870ce1247ceddb02a3860fa96d72543" - integrity sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg== +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.8.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127" - integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96" - integrity sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g== +"@babel/helper-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.4.tgz#59b373daaf3458e5747dece71bbaf45f9676af6d" + integrity sha512-inWpnHGgtg5NOF0eyHlC0/74/VkdRITY9dtTpB2PrxKKn+AkVMRiZz/Adrx+Ssg+MLDesi2zohBW6MVq6b4pOQ== dependencies: lodash "^4.17.13" -"@babel/helper-remap-async-to-generator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.1.tgz#bad6aaa4ff39ce8d4b82ccaae0bfe0f7dbb5f432" - integrity sha512-RfX1P8HqsfgmJ6CwaXGKMAqbYdlleqglvVtht0HGPMSsy2V6MqLlOJVF/0Qyb/m2ZCi2z3q3+s6Pv7R/dQuZ6A== +"@babel/helper-remap-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" + integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-wrap-function" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" - integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A== +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-simple-access@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" - integrity sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw== +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== dependencies: - "@babel/template" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helper-split-export-declaration@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" - integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g== +"@babel/helper-split-export-declaration@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" + integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== dependencies: - "@babel/types" "^7.10.1" + "@babel/types" "^7.10.4" -"@babel/helper-validator-identifier@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz#5770b0c1a826c4f53f5ede5e153163e0318e94b5" - integrity sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-wrap-function@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9" - integrity sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ== +"@babel/helper-wrap-function@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" + integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/helpers@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" - integrity sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw== +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== dependencies: - "@babel/template" "^7.10.1" - "@babel/traverse" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/highlight@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.1.tgz#841d098ba613ba1a427a2b383d79e35552c38ae0" - integrity sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg== +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== dependencies: - "@babel/helper-validator-identifier" "^7.10.1" + "@babel/helper-validator-identifier" "^7.10.4" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.1", "@babel/parser@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" - integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== +"@babel/parser@^7.10.4", "@babel/parser@^7.7.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.4.tgz#9eedf27e1998d87739fb5028a5120557c06a1a64" + integrity sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA== -"@babel/plugin-proposal-async-generator-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.1.tgz#6911af5ba2e615c4ff3c497fe2f47b35bf6d7e55" - integrity sha512-vzZE12ZTdB336POZjmpblWfNNRpMSua45EYnRigE2XsZxcXcIyly2ixnTJasJE4Zq3U7t2d8rRF7XRUuzHxbOw== +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz#4b65abb3d9bacc6c657aaa413e56696f9f170fc6" + integrity sha512-MJbxGSmejEFVOANAezdO39SObkURO5o/8b6fSH6D1pi9RZQt+ldppKPXfqgUWpSQ9asM6xaSaSJIaeWMDRP0Zg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-remap-async-to-generator" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.10.1", "@babel/plugin-proposal-class-properties@^7.8.3": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01" - integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw== +"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-dynamic-import@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0" - integrity sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA== +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" + integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09" - integrity sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg== +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" + integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78" - integrity sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123" - integrity sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA== +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" + integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-syntax-numeric-separator" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.10.1", "@babel/plugin-proposal-object-rest-spread@^7.9.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.1.tgz#cba44908ac9f142650b4a65b8aa06bf3478d5fb6" - integrity sha512-Z+Qri55KiQkHh7Fc4BW6o+QBuTagbOp9txE+4U1i79u9oWlf2npkiDx+Rf3iK3lbcHBuNy9UOkwuR5wOMH3LIQ== +"@babel/plugin-proposal-object-rest-spread@^7.10.4", "@babel/plugin-proposal-object-rest-spread@^7.9.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.4.tgz#50129ac216b9a6a55b3853fdd923e74bf553a4c0" + integrity sha512-6vh4SqRuLLarjgeOf4EaROJAHjvu9Gl+/346PbDH9yWbJyfnJ/ah3jmYKYtswEyCoWZiidvVHjHshd4WgjB9BA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.1" + "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-optional-catch-binding@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz#c9f86d99305f9fa531b568ff5ab8c964b8b223d2" - integrity sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA== +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" + integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.1.tgz#15f5d6d22708629451a91be28f8facc55b0e818c" - integrity sha512-dqQj475q8+/avvok72CF3AOSV/SGEcH29zT5hhohqqvvZ2+boQoOr7iGldBG5YXTO2qgCgc2B3WvVLUdbeMlGA== +"@babel/plugin-proposal-optional-chaining@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.4.tgz#750f1255e930a1f82d8cdde45031f81a0d0adff7" + integrity sha512-ZIhQIEeavTgouyMSdZRap4VPPHqJJ3NEs2cuHs5p0erH+iz6khB0qfgU8g7UuJkG88+fBMy23ZiU+nuHvekJeQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-private-methods@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598" - integrity sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg== +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f" - integrity sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ== +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" + integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-async-generators@^7.8.0": version "7.8.4" @@ -354,12 +354,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5" - integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ== +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" @@ -382,12 +382,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99" - integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg== +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.0": version "7.8.3" @@ -410,338 +410,338 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz#8b8733f8c57397b3eaa47ddba8841586dcaef362" - integrity sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ== +"@babel/plugin-syntax-top-level-await@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" + integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b" - integrity sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA== +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" + integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz#e5153eb1a3e028f79194ed8a7a4bf55f862b2062" - integrity sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg== +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" + integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-remap-async-to-generator" "^7.10.1" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" -"@babel/plugin-transform-block-scoped-functions@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz#146856e756d54b20fff14b819456b3e01820b85d" - integrity sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q== +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" + integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz#47092d89ca345811451cd0dc5d91605982705d5e" - integrity sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw== +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.4.tgz#a670d1364bb5019a621b9ea2001482876d734787" + integrity sha512-J3b5CluMg3hPUii2onJDRiaVbPtKFPLEaV5dOPY5OeAbDi1iU/UbbFFTgwb7WnanaDy7bjU35kc26W3eM5Qa0A== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.1.tgz#6e11dd6c4dfae70f540480a4702477ed766d733f" - integrity sha512-P9V0YIh+ln/B3RStPoXpEQ/CoAxQIhRSUn7aXqQ+FZJ2u8+oCtjIXR3+X0vsSD8zv+mb56K7wZW1XiDTDGiDRQ== +"@babel/plugin-transform-classes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" + integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-define-map" "^7.10.1" - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-optimise-call-expression" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.1.tgz#59aa399064429d64dce5cf76ef9b90b7245ebd07" - integrity sha512-mqSrGjp3IefMsXIenBfGcPXxJxweQe2hEIwMQvjtiDQ9b1IBvDUjkAtV/HMXX47/vXf14qDNedXsIiNd1FmkaQ== +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" + integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.10.1", "@babel/plugin-transform-destructuring@^7.8.8": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz#abd58e51337815ca3a22a336b85f62b998e71907" - integrity sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA== +"@babel/plugin-transform-destructuring@^7.10.4", "@babel/plugin-transform-destructuring@^7.8.8": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" + integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.10.1", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz#920b9fec2d78bb57ebb64a644d5c2ba67cc104ee" - integrity sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA== +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" + integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz#c900a793beb096bc9d4d0a9d0cde19518ffc83b9" - integrity sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA== +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" + integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz#279c3116756a60dd6e6f5e488ba7957db9c59eb3" - integrity sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA== +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" + integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-for-of@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz#ff01119784eb0ee32258e8646157ba2501fcfda5" - integrity sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w== +"@babel/plugin-transform-for-of@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" + integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz#4ed46fd6e1d8fde2a2ec7b03c66d853d2c92427d" - integrity sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw== +"@babel/plugin-transform-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" + integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== dependencies: - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz#5794f8da82846b22e4e6631ea1658bce708eb46a" - integrity sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw== +"@babel/plugin-transform-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" + integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz#90347cba31bca6f394b3f7bd95d2bbfd9fce2f39" - integrity sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA== +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" + integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-amd@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz#65950e8e05797ebd2fe532b96e19fc5482a1d52a" - integrity sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw== +"@babel/plugin-transform-modules-amd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.4.tgz#cb407c68b862e4c1d13a2fc738c7ec5ed75fc520" + integrity sha512-3Fw+H3WLUrTlzi3zMiZWp3AR4xadAEMv6XRCYnd5jAlLM61Rn+CRJaZMaNvIpcJpQ3vs1kyifYvEVPFfoSkKOA== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301" - integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg== +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-simple-access" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.1.tgz#9962e4b0ac6aaf2e20431ada3d8ec72082cbffb6" - integrity sha512-ewNKcj1TQZDL3YnO85qh9zo1YF1CHgmSTlRQgHqe63oTrMI85cthKtZjAiZSsSNjPQ5NCaYo5QkbYqEw1ZBgZA== +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.4.tgz#8f576afd943ac2f789b35ded0a6312f929c633f9" + integrity sha512-Tb28LlfxrTiOTGtZFsvkjpyjCl9IoaRI52AEU/VIwOwvDQWtbNJsAqTXzh+5R7i74e/OZHH2c2w2fsOqAfnQYQ== dependencies: - "@babel/helper-hoist-variables" "^7.10.1" - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz#ea080911ffc6eb21840a5197a39ede4ee67b1595" - integrity sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA== +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" + integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== dependencies: - "@babel/helper-module-transforms" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" - integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" + integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" -"@babel/plugin-transform-new-target@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz#6ee41a5e648da7632e22b6fb54012e87f612f324" - integrity sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw== +"@babel/plugin-transform-new-target@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" + integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-object-super@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz#2e3016b0adbf262983bf0d5121d676a5ed9c4fde" - integrity sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw== +"@babel/plugin-transform-object-super@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" + integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" -"@babel/plugin-transform-parameters@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd" - integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg== +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.4.tgz#7b4d137c87ea7adc2a0f3ebf53266871daa6fced" + integrity sha512-RurVtZ/D5nYfEg0iVERXYKEgDFeesHrHfx8RT05Sq57ucj2eOYAP6eu5fynL4Adju4I/mP/I6SO0DqNWAXjfLQ== dependencies: - "@babel/helper-get-function-arity" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz#cffc7315219230ed81dc53e4625bf86815b6050d" - integrity sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA== +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" + integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-regenerator@^7.10.1", "@babel/plugin-transform-regenerator@^7.8.7": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.1.tgz#10e175cbe7bdb63cc9b39f9b3f823c5c7c5c5490" - integrity sha512-B3+Y2prScgJ2Bh/2l9LJxKbb8C8kRfsG4AdPT+n7ixBHIxJaIG8bi8tgjxUMege1+WqSJ+7gu1YeoMVO3gPWzw== +"@babel/plugin-transform-regenerator@^7.10.4", "@babel/plugin-transform-regenerator@^7.8.7": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" + integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz#0fc1027312b4d1c3276a57890c8ae3bcc0b64a86" - integrity sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ== +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" + integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-runtime@^7.9.0": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1" - integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.4.tgz#594fb53453ea1b6f0779cceb48ce0718a447feb7" + integrity sha512-8ULlGv8p+Vuxu+kz2Y1dk6MYS2b/Dki+NO6/0ZlfSj5tMalfDL7jI/o/2a+rrWLqSXvnadEqc2WguB4gdQIxZw== dependencies: - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" resolve "^1.8.1" semver "^5.5.1" -"@babel/plugin-transform-shorthand-properties@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3" - integrity sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g== +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" + integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz#0c6d618a0c4461a274418460a28c9ccf5239a7c8" - integrity sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw== +"@babel/plugin-transform-spread@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.4.tgz#4e2c85ea0d6abaee1b24dcfbbae426fe8d674cff" + integrity sha512-1e/51G/Ni+7uH5gktbWv+eCED9pP8ZpRhZB3jOaI3mmzfvJTWHkuyYTv0Z5PYtyM+Tr2Ccr9kUdQxn60fI5WuQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-sticky-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz#90fc89b7526228bed9842cff3588270a7a393b00" - integrity sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA== +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" + integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/helper-regex" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-regex" "^7.10.4" -"@babel/plugin-transform-template-literals@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.1.tgz#914c7b7f4752c570ea00553b4284dad8070e8628" - integrity sha512-t7B/3MQf5M1T9hPCRG28DNGZUuxAuDqLYS03rJrIk2prj/UV7Z6FOneijhQhnv/Xa039vidXeVbvjK2SK5f7Gg== +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.4.tgz#e6375407b30fcb7fcfdbba3bb98ef3e9d36df7bc" + integrity sha512-4NErciJkAYe+xI5cqfS8pV/0ntlY5N5Ske/4ImxAVX7mk9Rxt2bwDTGv1Msc2BRJvWQcmYEC+yoMLdX22aE4VQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz#60c0239b69965d166b80a84de7315c1bc7e0bb0e" - integrity sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g== +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" + integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-escapes@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940" - integrity sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw== +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== dependencies: - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f" - integrity sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw== +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" + integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.9.0": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.2.tgz#715930f2cf8573b0928005ee562bed52fb65fdfb" - integrity sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.4.tgz#fbf57f9a803afd97f4f32e4f798bb62e4b2bef5f" + integrity sha512-tcmuQ6vupfMZPrLrc38d0sF2OjLT3/bZ0dry5HchNCQbrokoQi4reXqclvkkAT5b+gWc23meVWpve5P/7+w/zw== dependencies: - "@babel/compat-data" "^7.10.1" - "@babel/helper-compilation-targets" "^7.10.2" - "@babel/helper-module-imports" "^7.10.1" - "@babel/helper-plugin-utils" "^7.10.1" - "@babel/plugin-proposal-async-generator-functions" "^7.10.1" - "@babel/plugin-proposal-class-properties" "^7.10.1" - "@babel/plugin-proposal-dynamic-import" "^7.10.1" - "@babel/plugin-proposal-json-strings" "^7.10.1" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.1" - "@babel/plugin-proposal-numeric-separator" "^7.10.1" - "@babel/plugin-proposal-object-rest-spread" "^7.10.1" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.1" - "@babel/plugin-proposal-optional-chaining" "^7.10.1" - "@babel/plugin-proposal-private-methods" "^7.10.1" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.1" + "@babel/compat-data" "^7.10.4" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.10.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.10.4" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.1" + "@babel/plugin-syntax-class-properties" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.10.1" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.1" - "@babel/plugin-transform-arrow-functions" "^7.10.1" - "@babel/plugin-transform-async-to-generator" "^7.10.1" - "@babel/plugin-transform-block-scoped-functions" "^7.10.1" - "@babel/plugin-transform-block-scoping" "^7.10.1" - "@babel/plugin-transform-classes" "^7.10.1" - "@babel/plugin-transform-computed-properties" "^7.10.1" - "@babel/plugin-transform-destructuring" "^7.10.1" - "@babel/plugin-transform-dotall-regex" "^7.10.1" - "@babel/plugin-transform-duplicate-keys" "^7.10.1" - "@babel/plugin-transform-exponentiation-operator" "^7.10.1" - "@babel/plugin-transform-for-of" "^7.10.1" - "@babel/plugin-transform-function-name" "^7.10.1" - "@babel/plugin-transform-literals" "^7.10.1" - "@babel/plugin-transform-member-expression-literals" "^7.10.1" - "@babel/plugin-transform-modules-amd" "^7.10.1" - "@babel/plugin-transform-modules-commonjs" "^7.10.1" - "@babel/plugin-transform-modules-systemjs" "^7.10.1" - "@babel/plugin-transform-modules-umd" "^7.10.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.10.1" - "@babel/plugin-transform-object-super" "^7.10.1" - "@babel/plugin-transform-parameters" "^7.10.1" - "@babel/plugin-transform-property-literals" "^7.10.1" - "@babel/plugin-transform-regenerator" "^7.10.1" - "@babel/plugin-transform-reserved-words" "^7.10.1" - "@babel/plugin-transform-shorthand-properties" "^7.10.1" - "@babel/plugin-transform-spread" "^7.10.1" - "@babel/plugin-transform-sticky-regex" "^7.10.1" - "@babel/plugin-transform-template-literals" "^7.10.1" - "@babel/plugin-transform-typeof-symbol" "^7.10.1" - "@babel/plugin-transform-unicode-escapes" "^7.10.1" - "@babel/plugin-transform-unicode-regex" "^7.10.1" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.10.4" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.10.2" + "@babel/types" "^7.10.4" browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" @@ -760,42 +760,42 @@ esutils "^2.0.2" "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" - integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" + integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" - integrity sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig== +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/parser" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.1": - version "7.10.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.1.tgz#bbcef3031e4152a6c0b50147f4958df54ca0dd27" - integrity sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.7.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.4.tgz#e642e5395a3b09cc95c8e74a27432b484b697818" + integrity sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q== dependencies: - "@babel/code-frame" "^7.10.1" - "@babel/generator" "^7.10.1" - "@babel/helper-function-name" "^7.10.1" - "@babel/helper-split-export-declaration" "^7.10.1" - "@babel/parser" "^7.10.1" - "@babel/types" "^7.10.1" + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.4.4": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d" - integrity sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng== +"@babel/types@^7.10.4", "@babel/types@^7.4.4", "@babel/types@^7.7.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.4.tgz#369517188352e18219981efd156bfdb199fff1ee" + integrity sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg== dependencies: - "@babel/helper-validator-identifier" "^7.10.1" + "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.13" to-fast-properties "^2.0.0" @@ -805,9 +805,9 @@ integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== "@fortawesome/fontawesome-free@^5.11.2": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" - integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6" + integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -830,6 +830,11 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@popperjs/core@^2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398" + integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg== + "@rails/ujs@^6.0.2-1": version "6.0.3" resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.0.3.tgz#e68a03278e30daea6a110aac5dfa33c60c53055d" @@ -880,9 +885,9 @@ webpack-sources "^1.4.3" "@stylelint/postcss-css-in-js@^0.37.1": - version "0.37.1" - resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.1.tgz#41e5e7660f73d88227610e18c6ebb262d56ac125" - integrity sha512-UMf2Rni3JGKi3ZwYRGMYJ5ipOA5ENJSKMtYA/pE1ZLURwdh7B5+z2r73RmWvub+N0UuH1Lo+TGfCgYwPvqpXNw== + version "0.37.2" + resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" + integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== dependencies: "@babel/core" ">=7.9.0" @@ -900,17 +905,17 @@ integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== "@types/glob@^7.1.1": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" - integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== dependencies: "@types/minimatch" "*" "@types/node" "*" "@types/json-schema@^7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" - integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== "@types/minimatch@*": version "3.0.3" @@ -923,9 +928,9 @@ integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= "@types/node@*": - version "14.0.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.12.tgz#9c1d8ffb8084e8936603a6122a7649e40e68e04b" - integrity sha512-/sjzehvjkkpvLpYtN6/2dv5kg41otMGuHQUt9T2aiAuIfleCQRQHXXzF1eAw/qkZTj5Kcf4JSTf7EIizHocy6Q== + version "14.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" + integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1125,10 +1130,10 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" - integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== +acorn@^7.2.0: + version "7.3.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" + integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== aggregate-error@^3.0.0: version "3.0.1" @@ -1144,14 +1149,14 @@ ajv-errors@^1.0.0: integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + version "3.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" + integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5: - version "6.12.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" - integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1173,12 +1178,10 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== - dependencies: - type-fest "^0.11.0" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-html@0.0.7: version "0.0.7" @@ -1400,16 +1403,16 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^9.6.1, autoprefixer@^9.8.0: - version "9.8.0" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.0.tgz#68e2d2bef7ba4c3a65436f662d0a56a741e56511" - integrity sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A== + version "9.8.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" + integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001061" - chalk "^2.4.2" + caniuse-lite "^1.0.30001097" + colorette "^1.2.0" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.30" + postcss "^7.0.32" postcss-value-parser "^4.1.0" aws-sign2@~0.7.0: @@ -1422,6 +1425,18 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== +babel-eslint@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" + integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" + eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" + babel-loader@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" @@ -1489,11 +1504,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1505,9 +1515,9 @@ binary-extensions@^1.0.0: integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== bindings@^1.5.0: version "1.5.0" @@ -1677,14 +1687,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.4, browserslist@^4.8.5: - version "4.12.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" - integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== + version "4.13.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" + integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== dependencies: - caniuse-lite "^1.0.30001043" - electron-to-chromium "^1.3.413" - node-releases "^1.1.53" - pkg-up "^2.0.0" + caniuse-lite "^1.0.30001093" + electron-to-chromium "^1.3.488" + escalade "^3.0.1" + node-releases "^1.1.58" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1882,10 +1892,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061: - version "1.0.30001079" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001079.tgz#ed3e5225cd9a6850984fdd88bf24ce45d69b9c22" - integrity sha512-2KaYheg0iOY+CMmDuAB3DHehrXhhb4OZU4KBVGDr/YKyYAcpudaiUQ9PJ9rxrPlKEoJ3ATasQ5AN48MqpwS43Q== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097: + version "1.0.30001099" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001099.tgz#540118fcc6842d1fde62f4ee5521d1ec6afdb40e" + integrity sha512-sdS9A+sQTk7wKoeuZBN/YMAHVztUfVnjDi4/UV3sDE8xoh7YR12hKW+pIdB3oqKGwr9XaFL2ovfzt9w8eUI5CA== case-sensitive-paths-webpack-plugin@^2.3.0: version "2.3.0" @@ -1911,15 +1921,6 @@ chalk@2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@2.4.2, chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1931,18 +1932,19 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== +chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" -chalk@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" - integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -1967,11 +1969,6 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2041,18 +2038,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" @@ -2154,6 +2139,11 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" +colorette@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2374,7 +2364,15 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2385,13 +2383,14 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - lru-cache "^4.0.1" - which "^1.2.9" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" crypto-browserify@^3.11.0: version "3.12.0" @@ -2439,22 +2438,22 @@ css-has-pseudo@^0.10.0: postcss-selector-parser "^5.0.0-rc.4" css-loader@^3.4.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" - integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== + version "3.6.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" + integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.27" + postcss "^7.0.32" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.3" - schema-utils "^2.6.6" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" semver "^6.3.0" css-prefers-color-scheme@^3.1.1: @@ -2609,13 +2608,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug-loader@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/debug-loader/-/debug-loader-0.0.1.tgz#44dc37e09e3c39e6af334681960f70a534a9d056" - integrity sha1-RNw34J48OeavM0aBlg9wpTSp0FY= - dependencies: - loader-utils "^0.2.12" - debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2623,7 +2615,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2720,7 +2712,7 @@ deep-equal@^1.0.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= @@ -2934,15 +2926,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.413: - version "1.3.465" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.465.tgz#d692e5c383317570c2bd82092a24a0308c6ccf29" - integrity sha512-K/lUeT3NLAsJ5SHRDhK3/zd0tw7OUllYD8w+fTOXm6ljCPsp2qq+vMzxpLo8u1M27ZjZAjRbsA6rirvne2nAMQ== +electron-to-chromium@^1.3.488: + version "1.3.496" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.496.tgz#3f43d32930481d82ad3663d79658e7c59a58af0b" + integrity sha512-TXY4mwoyowwi4Lsrq9vcTUYBThyc1b2hXaTZI13p8/FRhY2CTaq5lK+DVjhYkKiTLsKt569Xes+0J5JsVXFurQ== elliptic@^6.0.0, elliptic@^6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -2962,11 +2954,6 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -2984,24 +2971,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" - integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== +enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" + integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== dependencies: graceful-fs "^4.1.2" memory-fs "^0.5.0" tapable "^1.0.0" +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -3027,21 +3012,21 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: - version "1.17.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" - integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" + is-callable "^1.2.0" + is-regex "^1.1.0" object-inspect "^1.7.0" object-keys "^1.1.1" object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" es-to-primitive@^1.2.1: version "1.2.1" @@ -3057,6 +3042,11 @@ es6-object-assign@^1.0.3: resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= +escalade@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.1.tgz#52568a77443f6927cd0ab9c73129137533c965ed" + integrity sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3078,6 +3068,13 @@ eslint-loader@^4.0.0: object-hash "^2.0.3" schema-utils "^2.6.5" +eslint-plugin-babel@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" + integrity sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g== + dependencies: + eslint-rule-composer "^0.3.0" + eslint-plugin-ignore-erb@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-ignore-erb/-/eslint-plugin-ignore-erb-0.1.1.tgz#951497d935c7d8a713a67f6bbbaa92e29bf0826d" @@ -3085,6 +3082,11 @@ eslint-plugin-ignore-erb@^0.1.1: dependencies: requireindex "~1.1.0" +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -3093,7 +3095,7 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^5.0.0: +eslint-scope@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== @@ -3101,34 +3103,35 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa" - integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ== +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint@^6.0.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== +eslint@^7.0.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f" + integrity sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" + enquirer "^2.3.5" + eslint-scope "^5.1.0" + eslint-utils "^2.0.0" + eslint-visitor-keys "^1.2.0" + espree "^7.1.0" + esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" @@ -3137,40 +3140,38 @@ eslint@^6.0.0: ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" + levn "^0.4.1" lodash "^4.17.14" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.3" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" table "^5.2.3" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== +espree@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" + integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== dependencies: - acorn "^7.1.1" + acorn "^7.2.0" acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" + eslint-visitor-keys "^1.2.0" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: +esquery@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== @@ -3204,11 +3205,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -ev-emitter@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" - integrity sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q== - eventemitter3@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" @@ -3274,11 +3270,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expose-loader@^0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.5.tgz#e29ea2d9aeeed3254a3faa1b35f502db9f9c3f6f" - integrity sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw== - express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3335,15 +3326,6 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -3374,9 +3356,9 @@ fast-deep-equal@^3.1.1: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.1.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" - integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3390,7 +3372,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3428,13 +3410,6 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" @@ -3526,13 +3501,6 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -3548,7 +3516,7 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@3.0.0: +findup-sync@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== @@ -3586,11 +3554,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" - integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA== - dependencies: - debug "^3.0.0" + version "1.12.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" + integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== for-in@^1.0.2: version "1.0.2" @@ -3828,7 +3794,7 @@ global-modules@1.0.0, global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" -global-modules@2.0.0, global-modules@^2.0.0: +global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== @@ -4179,7 +4145,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4213,13 +4179,6 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -imagesloaded@>=3.0.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/imagesloaded/-/imagesloaded-4.1.4.tgz#1376efcd162bb768c34c3727ac89cc04051f3cc7" - integrity sha512-ltiBVcYpc/TYTF5nolkMNsnREHW+ICvfQ3Yla2Sgr71YFwQ86bDwV9hgpFhFtrGPuwEx5+LqOHIrdXBdoWwwsA== - dependencies: - ev-emitter "^1.0.0" - import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -4255,7 +4214,7 @@ import-lazy@^4.0.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== -import-local@2.0.0, import-local@^2.0.0: +import-local@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== @@ -4323,25 +4282,6 @@ ini@^1.3.4, ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" - integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^3.0.0" - cli-cursor "^3.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.15" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.5.3" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -4350,12 +4290,7 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -interpret@^1.0.0: +interpret@^1.0.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== @@ -4468,7 +4403,7 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4, is-callable@^1.1.5: +is-callable@^1.1.4, is-callable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== @@ -4648,7 +4583,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4, is-regex@^1.0.5: +is-regex@^1.0.4, is-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== @@ -4759,15 +4694,15 @@ jquery-ui@^1.12.1: resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51" integrity sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE= -jquery@>=1.6.0: +jquery@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== js-base64@^2.1.8: - version "2.5.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" - integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== + version "2.6.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.3.tgz#7afdb9b57aa7717e15d370b66e8f36a9cb835dc3" + integrity sha512-fiUvdfCaAXoQTHdKMgTvg6IkecXDcVz6V5rlftUTclF9IKBjMizvSdQaCl/z/6TApDeby5NL+axYou3i0mu1Pg== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -4835,11 +4770,6 @@ json3@^3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -4932,13 +4862,13 @@ levenary@^1.1.1: dependencies: leven "^3.1.0" -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + prelude-ls "^1.2.1" + type-check "~0.4.0" lines-and-columns@^1.1.6: version "1.1.6" @@ -4961,25 +4891,6 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -loader-utils@^0.2.12: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" @@ -4998,14 +4909,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -5067,9 +4970,9 @@ lodash.uniq@^4.5.0: integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.5, lodash@~4.17.10: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== log-symbols@^2.2.0: version "2.2.0" @@ -5233,7 +5136,7 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@^0.4.1: +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -5361,7 +5264,7 @@ mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -5511,11 +5414,6 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - mz@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -5558,9 +5456,9 @@ negotiator@0.6.2: integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== neo-async@^2.5.0, neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== nice-try@^1.0.4: version "1.0.5" @@ -5619,10 +5517,10 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.53: - version "1.1.58" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" - integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== +node-releases@^1.1.58: + version "1.1.59" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.59.tgz#4d648330641cec704bff10f8e4fe28e453ab8e8e" + integrity sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw== node-sass@^4.13.1: version "4.14.1" @@ -5797,9 +5695,9 @@ object-hash@^2.0.3: integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== object-is@^1.0.1: version "1.1.2" @@ -5880,13 +5778,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== - dependencies: - mimic-fn "^2.1.0" - opn@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -5902,17 +5793,17 @@ optimize-css-assets-webpack-plugin@^5.0.3: cssnano "^4.1.10" last-call-webpack-plugin "^3.0.0" -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" original@^1.0.0: version "1.0.2" @@ -5931,7 +5822,7 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -5940,7 +5831,7 @@ os-locale@^3.0.0, os-locale@^3.1.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -5968,13 +5859,6 @@ p-is-promise@^2.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5982,13 +5866,6 @@ p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: dependencies: p-try "^2.0.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -6022,11 +5899,6 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6164,6 +6036,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -6255,13 +6132,6 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - pnp-webpack-plugin@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" @@ -6958,7 +6828,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== @@ -6972,7 +6842,7 @@ postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: indexes-of "^1.0.1" uniq "^1.0.1" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -6981,21 +6851,16 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.1 source-map "^0.6.1" supports-color "^6.1.0" -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prepend-http@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -7111,14 +6976,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -qtip2@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/qtip2/-/qtip2-3.0.3.tgz#7df088ae4412c24a4064de69e824cb3cf76210dc" - integrity sha1-ffCIrkQSwkpAZN5p6CTLPPdiENw= - dependencies: - imagesloaded ">=3.0.0" - jquery ">=1.6.0" - query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -7185,11 +7042,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -raw-loader@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" - integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= - read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -7312,12 +7164,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== regenerator-transform@^0.14.2: - version "0.14.4" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7" - integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw== + version "0.14.5" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== dependencies: "@babel/runtime" "^7.8.4" - private "^0.1.8" regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" @@ -7335,10 +7186,10 @@ regexp.prototype.flags@^1.2.0: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== regexpu-core@^4.7.0: version "4.7.0" @@ -7561,14 +7412,6 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2 dependencies: path-parse "^1.0.6" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -7623,11 +7466,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - run-parallel@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" @@ -7640,13 +7478,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.5.3: - version "6.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" - integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== - dependencies: - tslib "^1.9.0" - safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -7704,7 +7535,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== @@ -7713,13 +7544,6 @@ schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: ajv "^6.12.2" ajv-keywords "^3.4.1" -script-loader@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7" - integrity sha512-UMNLEvgOAQuzK8ji8qIscM3GIrRCWN6MmMXGD4SD5l6cSycgGsCo0tX5xRnfQcoghqct0tjHjcykgI1PyBE2aA== - dependencies: - raw-loader "~0.5.1" - scss-tokenizer@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" @@ -7757,11 +7581,16 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^6.0.0, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.2.1: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -7873,11 +7702,23 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + shelljs@^0.8.1: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" @@ -8225,7 +8066,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -8234,7 +8075,7 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimend@^1.0.0: +string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== @@ -8242,25 +8083,7 @@ string.prototype.trimend@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trimleft@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc" - integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - string.prototype.trimstart "^1.0.0" - -string.prototype.trimright@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3" - integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - string.prototype.trimend "^1.0.0" - -string.prototype.trimstart@^1.0.0: +string.prototype.trimstart@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== @@ -8361,15 +8184,10 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== - -stupid-table-plugin@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/stupid-table-plugin/-/stupid-table-plugin-1.1.3.tgz#e0d72038da8299727ada73545d2eb9c1be443056" - integrity sha1-4NcgONqCmXJ62nNUXS65wb5EMFY= +strip-json-comments@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^1.1.3: version "1.2.1" @@ -8406,15 +8224,15 @@ stylelint-config-standard@^20.0.0: stylelint-config-recommended "^3.0.0" stylelint@^13.0.0: - version "13.6.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.0.tgz#3528bc402a71f2af2a3de32fa4e9f1c24e49666d" - integrity sha512-55gG2pNjVr183JJM/tlr3KAua6vTVX7Ho/lgKKuCIWszTZ1gmrXjX4Wok53SI8wRYFPbwKAcJGULQ77OJxTcNw== + version "13.6.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" + integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== dependencies: "@stylelint/postcss-css-in-js" "^0.37.1" "@stylelint/postcss-markdown" "^0.36.1" autoprefixer "^9.8.0" balanced-match "^1.0.0" - chalk "^4.0.0" + chalk "^4.1.0" cosmiconfig "^6.0.0" debug "^4.1.1" execall "^2.0.0" @@ -8466,13 +8284,6 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supports-color@6.1.0, supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -8485,6 +8296,13 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.0.0, supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -8584,9 +8402,9 @@ terser-webpack-plugin@^2.3.5: webpack-sources "^1.4.3" terser@^4.1.2, terser@^4.6.12: - version "4.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006" - integrity sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw== + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -8605,9 +8423,9 @@ thenify-all@^1.0.0: thenify ">= 3.1.0 < 4" "thenify@>= 3.1.0 < 4": - version "3.3.0" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" - integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk= + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== dependencies: any-promise "^1.0.0" @@ -8619,7 +8437,7 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@^2.3.6, through@^2.3.8: +through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -8641,12 +8459,12 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== +tippy.js@^6.2.3: + version "6.2.5" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.5.tgz#5335e28228af5e22c524fe5e8a94654514f34b92" + integrity sha512-UIf8G99PMXGmdWPrr36s/DjQBdfxMPwzvPUXsxs3tDFDTZ1SgvKG+Jvt6RJ+aBqYL0oe/STxh3MNkCV3IWAKmw== dependencies: - os-tmpdir "~1.0.2" + "@popperjs/core" "^2.4.4" to-arraybuffer@^1.0.0: version "1.0.1" @@ -8780,17 +8598,12 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + prelude-ls "^1.2.1" type-fest@^0.13.1: version "0.13.1" @@ -8987,17 +8800,17 @@ unist-util-stringify-position@^2.0.0: "@types/unist" "^2.0.2" unist-util-visit-parents@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz#d4076af3011739c71d2ce99d05de37d545f4351d" - integrity sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g== + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5" + integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw== dependencies: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" unist-util-visit@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.2.tgz#3843782a517de3d2357b4c193b24af2d9366afb7" - integrity sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== dependencies: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" @@ -9103,11 +8916,6 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== - v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" @@ -9208,21 +9016,21 @@ webpack-assets-manifest@^3.1.1: webpack-sources "^1.0.0" webpack-cli@^3.3.0, webpack-cli@^3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" - integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" webpack-dev-middleware@^3.7.2: version "3.7.2" @@ -9352,6 +9160,13 @@ which@1, which@^1.2.14, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -9359,7 +9174,7 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -word-wrap@~1.2.3: +word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -9455,7 +9270,7 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.0, yargs-parser@^13.1.2: +yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -9489,23 +9304,6 @@ yargs@12.0.5: y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"